Skip to content

Commit ecbcd09

Browse files
committed
feat(react-core): useMemories() is a no-arg consumer of the core memory store
1 parent 0c1acf3 commit ecbcd09

3 files changed

Lines changed: 125 additions & 140 deletions

File tree

packages/react-core/src/v2/hooks/__tests__/use-memories.test.tsx

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { act, renderHook, waitFor } from "@testing-library/react";
2-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import {
3+
afterAll,
4+
afterEach,
5+
beforeEach,
6+
describe,
7+
expect,
8+
it,
9+
vi,
10+
} from "vitest";
311
import type { Mock } from "vitest";
412
import { useCopilotKit } from "../../context";
5-
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
13+
import { ɵcreateMemoryStore } from "@copilotkit/core";
14+
import type { ɵMemoryStore } from "@copilotkit/core";
615
import { useMemories } from "../use-memories";
716

817
vi.mock("../../context", () => ({
@@ -11,7 +20,6 @@ vi.mock("../../context", () => ({
1120

1221
const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
1322

14-
const AGENT_ID = "agent-1";
1523
const RUNTIME_URL = "https://runtime.example.com";
1624

1725
type WireMemory = {
@@ -72,42 +80,74 @@ function makeFetchMock(
7280
});
7381
}
7482

75-
let registeredStore: unknown;
76-
let fetchMock: Mock;
83+
// Stub the global fetch once for the entire module — RxJS's fromFetch always
84+
// calls globalThis.fetch, so injected fetch in ɵcreateMemoryStore is a
85+
// pass-through. vi.stubGlobal restores the original automatically on afterAll.
86+
const fetchMock = vi.fn();
87+
vi.stubGlobal("fetch", fetchMock);
7788

78-
function setupCopilotKit(): void {
79-
registeredStore = undefined;
89+
let store: ɵMemoryStore;
90+
91+
function setupCopilotKit(mock: Mock): void {
92+
store = ɵcreateMemoryStore({ fetch: mock });
93+
store.start();
8094

8195
mockUseCopilotKit.mockReturnValue({
8296
copilotkit: {
83-
runtimeUrl: RUNTIME_URL,
84-
headers: {},
85-
intelligence: { wsUrl: "wss://gw.example.com/client" },
86-
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
87-
registerMemoryStore: vi.fn((_agentId: string, store: unknown) => {
88-
registeredStore = store;
89-
}),
90-
unregisterMemoryStore: vi.fn(),
97+
getMemoryStore: () => store,
9198
},
9299
executingToolCallIds: new Set(),
93100
});
94101
}
95102

103+
/**
104+
* Triggers the store's first context dispatch so the snapshot fetch fires.
105+
* Must be called inside `act(...)` after `renderHook` so the hook is already
106+
* subscribed when the async response arrives.
107+
*/
108+
function activateStore(): void {
109+
store.setContext({
110+
runtimeUrl: RUNTIME_URL,
111+
wsUrl: "wss://gw.example.com/client",
112+
headers: {},
113+
});
114+
}
115+
96116
describe("useMemories", () => {
97117
beforeEach(() => {
98-
setupCopilotKit();
118+
fetchMock.mockReset();
119+
mockUseCopilotKit.mockReset();
120+
// Default: empty snapshot with no mutations expected.
121+
fetchMock.mockImplementation(makeFetchMock([]));
122+
setupCopilotKit(fetchMock);
99123
});
100124

101125
afterEach(() => {
102-
vi.unstubAllGlobals();
103126
vi.clearAllMocks();
104127
});
105128

129+
afterAll(() => {
130+
vi.unstubAllGlobals();
131+
});
132+
133+
it("exposes empty memories and isAvailable true on initial render", () => {
134+
const { result } = renderHook(() => useMemories());
135+
136+
expect(result.current.memories).toEqual([]);
137+
expect(result.current.isAvailable).toBe(true);
138+
});
139+
106140
it("loads the snapshot on mount", async () => {
107-
fetchMock = makeFetchMock([wireMemory("m1"), wireMemory("m2")]);
108-
vi.stubGlobal("fetch", fetchMock);
141+
fetchMock.mockImplementation(
142+
makeFetchMock([wireMemory("m1"), wireMemory("m2")]),
143+
);
144+
setupCopilotKit(fetchMock);
109145

110-
const { result } = renderHook(() => useMemories({ agentId: AGENT_ID }));
146+
const { result } = renderHook(() => useMemories());
147+
148+
act(() => {
149+
activateStore();
150+
});
111151

112152
await waitFor(() => {
113153
expect(result.current.memories.map((m) => m.id)).toEqual(["m1", "m2"]);
@@ -118,17 +158,23 @@ describe("useMemories", () => {
118158
`${RUNTIME_URL}/memories`,
119159
expect.objectContaining({ method: "GET" }),
120160
);
121-
expect(registeredStore).toBeDefined();
122161
});
123162

124163
it("addMemory POSTs and resolves to the created memory, adding it to the list", async () => {
125-
fetchMock = makeFetchMock([], () => ({
126-
...wireMemory("m1", "hi"),
127-
absorbed: false,
128-
}));
129-
vi.stubGlobal("fetch", fetchMock);
164+
fetchMock.mockImplementation(
165+
makeFetchMock([], () => ({
166+
...wireMemory("m1", "hi"),
167+
absorbed: false,
168+
})),
169+
);
170+
setupCopilotKit(fetchMock);
171+
172+
const { result } = renderHook(() => useMemories());
173+
174+
act(() => {
175+
activateStore();
176+
});
130177

131-
const { result } = renderHook(() => useMemories({ agentId: AGENT_ID }));
132178
await waitFor(() => expect(result.current.isLoading).toBe(false));
133179

134180
let created: { id: string } | undefined;
@@ -150,13 +196,20 @@ describe("useMemories", () => {
150196
});
151197

152198
it("updateMemory supersedes: resolves to the new id and the list shows it (old id gone)", async () => {
153-
fetchMock = makeFetchMock([wireMemory("m1", "old")], () => ({
154-
...wireMemory("m2", "new"),
155-
retiredId: "m1",
156-
}));
157-
vi.stubGlobal("fetch", fetchMock);
199+
fetchMock.mockImplementation(
200+
makeFetchMock([wireMemory("m1", "old")], () => ({
201+
...wireMemory("m2", "new"),
202+
retiredId: "m1",
203+
})),
204+
);
205+
setupCopilotKit(fetchMock);
206+
207+
const { result } = renderHook(() => useMemories());
208+
209+
act(() => {
210+
activateStore();
211+
});
158212

159-
const { result } = renderHook(() => useMemories({ agentId: AGENT_ID }));
160213
await waitFor(() =>
161214
expect(result.current.memories.map((m) => m.id)).toEqual(["m1"]),
162215
);
@@ -180,10 +233,15 @@ describe("useMemories", () => {
180233
});
181234

182235
it("removeMemory DELETEs and removes the memory from the list", async () => {
183-
fetchMock = makeFetchMock([wireMemory("m1")]);
184-
vi.stubGlobal("fetch", fetchMock);
236+
fetchMock.mockImplementation(makeFetchMock([wireMemory("m1")]));
237+
setupCopilotKit(fetchMock);
238+
239+
const { result } = renderHook(() => useMemories());
240+
241+
act(() => {
242+
activateStore();
243+
});
185244

186-
const { result } = renderHook(() => useMemories({ agentId: AGENT_ID }));
187245
await waitFor(() =>
188246
expect(result.current.memories.map((m) => m.id)).toEqual(["m1"]),
189247
);

packages/react-core/src/v2/hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type { UseInterruptConfig } from "./use-interrupt";
1818
export { useThreads } from "./use-threads";
1919
export type { Thread, UseThreadsInput, UseThreadsResult } from "./use-threads";
2020
export { useMemories } from "./use-memories";
21-
export type { UseMemoriesInput, UseMemoriesResult } from "./use-memories";
21+
export type { UseMemoriesResult } from "./use-memories";
2222
export { useLearnFromUserAction } from "./use-learn-from-user-action";
2323
export type {
2424
LearnFromUserActionInput,

0 commit comments

Comments
 (0)