convex-durable-agents 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -0
  2. package/dist/client/handler.d.ts +2 -0
  3. package/dist/client/handler.d.ts.map +1 -1
  4. package/dist/client/handler.js +1 -0
  5. package/dist/client/handler.js.map +1 -1
  6. package/dist/client/streamer.d.ts +3 -1
  7. package/dist/client/streamer.d.ts.map +1 -1
  8. package/dist/client/streamer.js +9 -3
  9. package/dist/client/streamer.js.map +1 -1
  10. package/dist/react/test/happy-dom-setup.d.ts +2 -0
  11. package/dist/react/test/happy-dom-setup.d.ts.map +1 -0
  12. package/dist/react/test/happy-dom-setup.js +28 -0
  13. package/dist/react/test/happy-dom-setup.js.map +1 -0
  14. package/package.json +24 -21
  15. package/src/client/handler.ts +3 -0
  16. package/src/client/streamer.test.ts +187 -0
  17. package/src/client/streamer.ts +10 -3
  18. package/src/client/tools.test.ts +48 -0
  19. package/src/component/messages.test.ts +40 -0
  20. package/src/component/streams.test.ts +118 -0
  21. package/src/component/threads.test.ts +48 -0
  22. package/src/react/__fixtures__/01-early-streaming-start.json +35 -0
  23. package/src/react/__fixtures__/02-reasoning-complete-tool-call.json +85 -0
  24. package/src/react/__fixtures__/03-new-round-seq2.json +89 -0
  25. package/src/react/__fixtures__/04-tool-call-error-seq3.json +145 -0
  26. package/src/react/__fixtures__/05-later-round-seq5.json +117 -0
  27. package/src/react/__fixtures__/06-text-streaming-seq6.json +162 -0
  28. package/src/react/__fixtures__/07-text-streaming-more-seq6.json +212 -0
  29. package/src/react/__fixtures__/08-fully-committed-seq6.json +188 -0
  30. package/src/react/__snapshots__/apply-streaming-updates.test.ts.snap +1357 -0
  31. package/src/react/__snapshots__/use-thread-messages.test.tsx.snap +1429 -0
  32. package/src/react/agent-chat.test.tsx +155 -0
  33. package/src/react/apply-streaming-updates.test.ts +28 -0
  34. package/src/react/test/happy-dom-setup.ts +31 -0
  35. package/src/react/use-thread-messages.test.tsx +702 -0
  36. package/src/utils/msg.test.ts +34 -0
  37. package/src/utils/retry.test.ts +214 -0
@@ -0,0 +1,155 @@
1
+ import "./test/happy-dom-setup";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import type { MessageDoc, ThreadDoc } from "./types";
5
+
6
+ const listMessagesQueryRef = { __brand: "listMessagesQuery" } as any;
7
+ const streamUpdatesQueryRef = { __brand: "streamUpdatesQuery" } as any;
8
+ const getThreadQueryRef = { __brand: "getThreadQuery" } as any;
9
+ const sendMutationRef = { __brand: "sendMutation" } as any;
10
+ const stopMutationRef = { __brand: "stopMutation" } as any;
11
+ const resumeMutationRef = { __brand: "resumeMutation" } as any;
12
+
13
+ let messagesReturn: MessageDoc[] | undefined;
14
+ let threadReturn: ThreadDoc | null | undefined;
15
+ let sendMutationCalls: Array<{ prompt: string; threadId: string }> = [];
16
+ let sendMutationImpl: (args: { prompt: string; threadId: string }) => Promise<null>;
17
+ let stopMutationImpl: (args: { threadId: string }) => Promise<null>;
18
+ let resumeMutationImpl: (args: { threadId: string; prompt?: string }) => Promise<null>;
19
+ type SendOptimisticUpdate = (localStore: any, args: { prompt: string; threadId: string }) => void;
20
+ let capturedOptimisticUpdate: SendOptimisticUpdate | undefined;
21
+
22
+ vi.doMock("convex/react", () => ({
23
+ useQuery: (ref: unknown, args: unknown) => {
24
+ if (args === "skip") return undefined;
25
+ if (ref === listMessagesQueryRef) return messagesReturn;
26
+ if (ref === getThreadQueryRef) return threadReturn;
27
+ if (ref === streamUpdatesQueryRef) return undefined;
28
+ return undefined;
29
+ },
30
+ useMutation: (ref: unknown) => {
31
+ if (ref === sendMutationRef) {
32
+ const mutation = (async (args: { prompt: string; threadId: string }) => {
33
+ sendMutationCalls.push(args);
34
+ return sendMutationImpl(args);
35
+ }) as ((args: { prompt: string; threadId: string }) => Promise<null>) & {
36
+ withOptimisticUpdate: (update: SendOptimisticUpdate) => typeof mutation;
37
+ };
38
+ mutation.withOptimisticUpdate = (update: SendOptimisticUpdate) => {
39
+ capturedOptimisticUpdate = update;
40
+ return mutation;
41
+ };
42
+ return mutation;
43
+ }
44
+ if (ref === stopMutationRef) {
45
+ return stopMutationImpl;
46
+ }
47
+ if (ref === resumeMutationRef) {
48
+ return resumeMutationImpl;
49
+ }
50
+ throw new Error("Unknown mutation reference");
51
+ },
52
+ }));
53
+
54
+ const { useAgentChat } = await import("./agent-chat");
55
+
56
+ function makeUserDoc(id: string, text: string): MessageDoc {
57
+ return {
58
+ _id: `doc-${id}`,
59
+ _creationTime: 1,
60
+ threadId: "thread-1",
61
+ id,
62
+ role: "user",
63
+ parts: [{ type: "text", text }],
64
+ };
65
+ }
66
+
67
+ beforeEach(() => {
68
+ messagesReturn = [];
69
+ threadReturn = { _id: "thread-doc-1", _creationTime: 500, status: "completed", stopSignal: false };
70
+ sendMutationCalls = [];
71
+ sendMutationImpl = async () => null;
72
+ stopMutationImpl = async () => null;
73
+ resumeMutationImpl = async () => null;
74
+ capturedOptimisticUpdate = undefined;
75
+ });
76
+
77
+ describe("useAgentChat optimistic updates", () => {
78
+ const options = {
79
+ listMessages: listMessagesQueryRef,
80
+ streamUpdates: streamUpdatesQueryRef,
81
+ getThread: getThreadQueryRef,
82
+ sendMessage: sendMutationRef,
83
+ stopThread: stopMutationRef,
84
+ resumeThread: resumeMutationRef,
85
+ threadId: "thread-1",
86
+ };
87
+
88
+ it("pre-binds threadId for sendMessage", async () => {
89
+ const { result } = renderHook(() => useAgentChat(options));
90
+
91
+ await act(async () => {
92
+ await result.current.sendMessage("hello");
93
+ });
94
+
95
+ expect(sendMutationCalls).toEqual([{ threadId: "thread-1", prompt: "hello" }]);
96
+ });
97
+
98
+ it("registers an optimistic updater that appends a user message", () => {
99
+ renderHook(() => useAgentChat(options));
100
+ expect(capturedOptimisticUpdate).toBeDefined();
101
+
102
+ let queryValue: MessageDoc[] | undefined = [makeUserDoc("existing", "existing prompt")];
103
+ let setQueryValue: MessageDoc[] | undefined;
104
+ let setQueryArgs: { threadId: string } | undefined;
105
+
106
+ capturedOptimisticUpdate!(
107
+ {
108
+ getAllQueries: () => [],
109
+ getQuery: (_query: unknown, args: unknown) => {
110
+ expect(_query).toBe(listMessagesQueryRef);
111
+ expect(args).toEqual({ threadId: "thread-1" });
112
+ return queryValue;
113
+ },
114
+ setQuery: (_query: unknown, args: unknown, value: unknown) => {
115
+ expect(_query).toBe(listMessagesQueryRef);
116
+ setQueryArgs = args as { threadId: string };
117
+ setQueryValue = value as MessageDoc[] | undefined;
118
+ queryValue = value as MessageDoc[] | undefined;
119
+ },
120
+ },
121
+ { threadId: "thread-1", prompt: "new prompt" },
122
+ );
123
+
124
+ expect(setQueryArgs).toEqual({ threadId: "thread-1" });
125
+ expect(setQueryValue).toBeDefined();
126
+ expect(setQueryValue).toHaveLength(2);
127
+ const optimisticMessage = setQueryValue![1]!;
128
+ expect(optimisticMessage.role).toBe("user");
129
+ expect(optimisticMessage.threadId).toBe("thread-1");
130
+ expect(optimisticMessage.id.startsWith("optimistic-")).toBe(true);
131
+ expect(optimisticMessage.parts).toEqual([{ type: "text", text: "new prompt" }]);
132
+ });
133
+
134
+ it("seeds query with optimistic message when list query has not loaded", () => {
135
+ renderHook(() => useAgentChat(options));
136
+ expect(capturedOptimisticUpdate).toBeDefined();
137
+
138
+ let setQueryValue: MessageDoc[] | undefined;
139
+
140
+ capturedOptimisticUpdate!(
141
+ {
142
+ getAllQueries: () => [],
143
+ getQuery: () => undefined,
144
+ setQuery: (_query: unknown, _args: unknown, value: unknown) => {
145
+ setQueryValue = value as MessageDoc[] | undefined;
146
+ },
147
+ },
148
+ { threadId: "thread-1", prompt: "first message" },
149
+ );
150
+
151
+ expect(setQueryValue).toBeDefined();
152
+ expect(setQueryValue).toHaveLength(1);
153
+ expect(setQueryValue![0]!.parts).toEqual([{ type: "text", text: "first message" }]);
154
+ });
155
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { applyStreamingUpdates } from "./use-thread-messages";
5
+
6
+ const FIXTURES_DIR = join(import.meta.dirname!, "__fixtures__");
7
+
8
+ type Fixture = {
9
+ description: string;
10
+ logLine: number;
11
+ messages: Parameters<typeof applyStreamingUpdates>[0];
12
+ streamingUpdates: Parameters<typeof applyStreamingUpdates>[1];
13
+ };
14
+
15
+ const fixtureFiles = readdirSync(FIXTURES_DIR)
16
+ .filter((f) => f.endsWith(".json"))
17
+ .sort();
18
+
19
+ describe("applyStreamingUpdates", () => {
20
+ for (const file of fixtureFiles) {
21
+ const fixture: Fixture = JSON.parse(readFileSync(join(FIXTURES_DIR, file), "utf-8"));
22
+
23
+ it(`${fixture.description} (log line ${fixture.logLine})`, async () => {
24
+ const result = await applyStreamingUpdates(fixture.messages, fixture.streamingUpdates);
25
+ expect(result).toMatchSnapshot();
26
+ });
27
+ }
28
+ });
@@ -0,0 +1,31 @@
1
+ import { Window } from "happy-dom";
2
+
3
+ const win = new Window({ url: "http://localhost" });
4
+
5
+ // Register DOM globals that @testing-library/react needs
6
+ const globals = [
7
+ "document",
8
+ "window",
9
+ "navigator",
10
+ "HTMLElement",
11
+ "HTMLInputElement",
12
+ "HTMLTextAreaElement",
13
+ "HTMLSelectElement",
14
+ "Element",
15
+ "Node",
16
+ "Text",
17
+ "DocumentFragment",
18
+ "MutationObserver",
19
+ "Event",
20
+ "CustomEvent",
21
+ "getComputedStyle",
22
+ ] as const;
23
+
24
+ for (const key of globals) {
25
+ if ((win as any)[key] !== undefined) {
26
+ (globalThis as any)[key] = (win as any)[key];
27
+ }
28
+ }
29
+
30
+ // Bind document.createElement etc. properly
31
+ globalThis.document = win.document as any;