@assistant-ui/react 0.15.14 → 0.15.16

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 (210) hide show
  1. package/dist/assistant-transport.d.ts +32 -0
  2. package/dist/assistant-transport.d.ts.map +1 -0
  3. package/dist/assistant-transport.js +27 -0
  4. package/dist/assistant-transport.js.map +1 -0
  5. package/dist/context/providers/MessageProvider.js +1 -1
  6. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  7. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  8. package/dist/devtools/DevToolsHooks.js +10 -12
  9. package/dist/devtools/DevToolsHooks.js.map +1 -1
  10. package/dist/hooks/useMessageQuote.js +1 -1
  11. package/dist/hooks/useToolCallElapsed.js +1 -1
  12. package/dist/index.d.ts +4 -7
  13. package/dist/index.js +6 -8
  14. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  15. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  16. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
  18. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  19. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  20. package/dist/mcp-apps/bridge.js +33 -24
  21. package/dist/mcp-apps/bridge.js.map +1 -1
  22. package/dist/model-context/makeAssistantVisible.js +1 -1
  23. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  24. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  25. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +5 -2
  26. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  27. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  29. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  32. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
  33. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  34. package/dist/primitives/attachment/AttachmentName.js +1 -1
  35. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  36. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  37. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
  38. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
  39. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  40. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  41. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  42. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  43. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  44. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  45. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  46. package/dist/primitives/composer/ComposerDictationTranscript.js +3 -3
  47. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  48. package/dist/primitives/composer/ComposerInput.js +2 -2
  49. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  50. package/dist/primitives/composer/ComposerQuote.js +1 -1
  51. package/dist/primitives/composer/ComposerRoot.js +1 -1
  52. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  53. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
  54. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  55. package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
  56. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  57. package/dist/primitives/composer/useComposerInputState.js +1 -1
  58. package/dist/primitives/message/MessageIf.js +1 -1
  59. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  60. package/dist/primitives/message/MessageRoot.js +1 -1
  61. package/dist/primitives/message.js +1 -1
  62. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  63. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  64. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  65. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  66. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  67. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  68. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  69. package/dist/primitives/thread/ThreadIf.js +1 -1
  70. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  71. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  72. package/dist/primitives/thread/ThreadRoot.js +44 -9
  73. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  74. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  75. package/dist/primitives/thread/ThreadViewport.js +2 -13
  76. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  77. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  78. package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
  79. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  80. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  81. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  82. package/dist/sandbox-host/SandboxHost.js +6 -1
  83. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  84. package/dist/unstable/useComposerInput.d.ts +2 -2
  85. package/dist/unstable/useComposerInput.js +1 -1
  86. package/dist/unstable/useComposerInput.js.map +1 -1
  87. package/dist/unstable/useComposerInputHistory.js +2 -2
  88. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  89. package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
  90. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  91. package/dist/unstable/useLiveCompletionAdapter.js +16 -3
  92. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  93. package/dist/unstable/useMentionAdapter.d.ts +1 -1
  94. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  95. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  96. package/dist/utils/invokeCallbackSafely.js +15 -0
  97. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  98. package/dist/utils/smooth/SmoothContext.d.ts +6 -6
  99. package/dist/utils/smooth/SmoothContext.js +1 -1
  100. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  101. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  102. package/dist/utils/smooth/useSmooth.js +16 -1
  103. package/dist/utils/smooth/useSmooth.js.map +1 -1
  104. package/dist/utils/useToolArgsFieldStatus.js +1 -1
  105. package/package.json +10 -10
  106. package/src/assistant-transport.ts +94 -0
  107. package/src/devtools/DevToolsHooks.test.ts +40 -0
  108. package/src/devtools/DevToolsHooks.ts +10 -15
  109. package/src/index.ts +8 -4
  110. package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
  111. package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
  112. package/src/mcp-apps/bridge.test.ts +153 -0
  113. package/src/mcp-apps/bridge.ts +42 -30
  114. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  115. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  116. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
  117. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
  118. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  119. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  120. package/src/primitives/composer/ComposerInput.tsx +1 -1
  121. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
  122. package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
  123. package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
  124. package/src/primitives/thread/ThreadRoot.tsx +33 -0
  125. package/src/primitives/thread/ThreadViewport.tsx +1 -8
  126. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
  127. package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
  128. package/src/sandbox-host/SandboxHost.test.tsx +90 -0
  129. package/src/sandbox-host/SandboxHost.tsx +8 -2
  130. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
  131. package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
  132. package/src/tests/augmentations.test.ts +49 -0
  133. package/src/tests/in-memory-thread-list.test.tsx +1 -1
  134. package/src/tests/threadMessageById.test.tsx +61 -1
  135. package/src/unstable/useComposerInput.ts +2 -2
  136. package/src/unstable/useComposerInputHistory.test.tsx +4 -4
  137. package/src/unstable/useComposerInputHistory.ts +1 -1
  138. package/src/unstable/useLiveCompletionAdapter.test.tsx +157 -0
  139. package/src/unstable/useLiveCompletionAdapter.ts +41 -17
  140. package/src/utils/invokeCallbackSafely.ts +11 -0
  141. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  142. package/src/utils/smooth/useSmooth.ts +20 -6
  143. package/dist/client/InMemoryThreadList.d.ts +0 -12
  144. package/dist/client/InMemoryThreadList.d.ts.map +0 -1
  145. package/dist/client/InMemoryThreadList.js +0 -374
  146. package/dist/client/InMemoryThreadList.js.map +0 -1
  147. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  148. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  149. package/dist/context/react/utils/ensureBinding.js +0 -24
  150. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  151. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  152. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  153. package/dist/context/react/utils/useRuntimeState.js +0 -33
  154. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  155. package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
  156. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
  157. package/dist/legacy-runtime/cloud/auiV0.js +0 -137
  158. package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
  159. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
  160. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
  161. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
  162. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
  163. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
  164. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
  165. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
  166. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
  167. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
  168. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
  169. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
  170. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
  171. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
  172. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
  173. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
  174. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
  175. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
  176. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
  177. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
  178. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
  179. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
  180. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
  181. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
  182. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
  183. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
  184. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
  185. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
  186. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
  187. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
  188. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
  189. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
  190. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
  191. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
  192. package/src/client/InMemoryThreadList.ts +0 -247
  193. package/src/context/react/utils/ensureBinding.ts +0 -46
  194. package/src/context/react/utils/useRuntimeState.ts +0 -46
  195. package/src/legacy-runtime/cloud/auiV0.ts +0 -331
  196. package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
  197. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
  198. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
  199. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
  200. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
  201. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
  202. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
  203. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
  204. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
  205. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
  206. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
  207. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
  208. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
  209. package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
  210. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
@@ -1,585 +0,0 @@
1
- // @vitest-environment jsdom
2
-
3
- import { act, render, waitFor } from "@testing-library/react";
4
- import type { FC } from "react";
5
- import { afterEach, describe, expect, it, vi } from "vitest";
6
- import { useAui } from "@assistant-ui/store";
7
- import { AssistantRuntimeProvider } from "../../../context";
8
- import {
9
- useAssistantTransportRuntime,
10
- useAssistantTransportSendCommand,
11
- } from "./useAssistantTransportRuntime";
12
- import type {
13
- AssistantTransportCommand,
14
- AssistantTransportOptions,
15
- AssistantTransportStateConverter,
16
- } from "./types";
17
-
18
- const converter: AssistantTransportStateConverter<unknown> = (
19
- _state,
20
- meta,
21
- ) => ({
22
- messages: [],
23
- isRunning: meta.isSending,
24
- });
25
-
26
- const createMessageCommand = (text: string): AssistantTransportCommand => ({
27
- type: "add-message",
28
- message: {
29
- role: "user",
30
- parts: [{ type: "text", text }],
31
- },
32
- parentId: null,
33
- sourceId: null,
34
- });
35
-
36
- const createStreamResponse = () => {
37
- let controller!: ReadableStreamDefaultController<Uint8Array>;
38
- const stream = new ReadableStream<Uint8Array>({
39
- start(c) {
40
- controller = c;
41
- },
42
- });
43
- return {
44
- response: new Response(stream, { status: 200 }),
45
- close: () => controller.close(),
46
- };
47
- };
48
-
49
- type StreamResponse = ReturnType<typeof createStreamResponse>;
50
-
51
- type RecordedRequest = {
52
- url: string;
53
- init: RequestInit;
54
- body: Record<string, any>;
55
- };
56
-
57
- const installFetch = () => {
58
- const requests: RecordedRequest[] = [];
59
- const servers: StreamResponse[] = [];
60
-
61
- vi.stubGlobal(
62
- "fetch",
63
- async (url: RequestInfo | URL, init: RequestInit = {}) => {
64
- requests.push({
65
- url: String(url),
66
- init,
67
- body: JSON.parse(init.body as string),
68
- });
69
- const server = createStreamResponse();
70
- servers.push(server);
71
- return server.response;
72
- },
73
- );
74
-
75
- return { requests, servers };
76
- };
77
-
78
- const installPendingFetch = () => {
79
- const requests: RecordedRequest[] = [];
80
- const pending: {
81
- resolve: (response: Response) => void;
82
- reject: (reason: unknown) => void;
83
- }[] = [];
84
-
85
- vi.stubGlobal("fetch", (url: RequestInfo | URL, init: RequestInit = {}) => {
86
- requests.push({
87
- url: String(url),
88
- init,
89
- body: JSON.parse(init.body as string),
90
- });
91
-
92
- return new Promise<Response>((resolve, reject) => {
93
- pending.push({ resolve, reject });
94
- init.signal?.addEventListener(
95
- "abort",
96
- () => reject(init.signal?.reason),
97
- {
98
- once: true,
99
- },
100
- );
101
- });
102
- });
103
-
104
- return { requests, pending };
105
- };
106
-
107
- const mountRuntime = (
108
- options?: Partial<AssistantTransportOptions<unknown>>,
109
- ) => {
110
- const captured: {
111
- aui?: ReturnType<typeof useAui>;
112
- sendCommand?: (command: AssistantTransportCommand) => void;
113
- } = {};
114
- const Capture: FC = () => {
115
- captured.aui = useAui();
116
- captured.sendCommand = useAssistantTransportSendCommand();
117
- return null;
118
- };
119
- const App: FC = () => {
120
- const runtime = useAssistantTransportRuntime({
121
- initialState: {},
122
- api: "https://example.com/api",
123
- headers: {},
124
- converter,
125
- ...options,
126
- });
127
- return (
128
- <AssistantRuntimeProvider runtime={runtime}>
129
- <Capture />
130
- </AssistantRuntimeProvider>
131
- );
132
- };
133
- const utils = render(<App />);
134
- return {
135
- aui: () => captured.aui!,
136
- sendCommand: (command: AssistantTransportCommand) =>
137
- captured.sendCommand!(command),
138
- ...utils,
139
- };
140
- };
141
-
142
- afterEach(() => {
143
- vi.unstubAllGlobals();
144
- vi.restoreAllMocks();
145
- });
146
-
147
- describe("useAssistantTransportRuntime", () => {
148
- it("no-ops a follow-up run that finds an empty queue", async () => {
149
- const fetchMock = installFetch();
150
- const onError = vi.fn();
151
- const { aui, sendCommand } = mountRuntime({ onError });
152
- await waitFor(() =>
153
- expect(
154
- (aui().thread.getState().extras as { sendCommand?: unknown })
155
- ?.sendCommand,
156
- ).toBeTypeOf("function"),
157
- );
158
-
159
- // Two synchronous sends coalesce into one request plus one follow-up run.
160
- act(() => {
161
- sendCommand(createMessageCommand("a"));
162
- sendCommand(createMessageCommand("b"));
163
- });
164
-
165
- await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
166
- expect(
167
- fetchMock.requests[0]!.body["commands"].map((c: any) => c.type),
168
- ).toEqual(["add-message", "add-message"]);
169
- expect(fetchMock.requests[0]!.body["state"]).toEqual({});
170
-
171
- act(() => fetchMock.servers[0]!.close());
172
-
173
- await waitFor(() => expect(aui().thread.getState().isRunning).toBe(false));
174
- await act(async () => {});
175
- expect(onError).not.toHaveBeenCalled();
176
- expect(fetchMock.requests).toHaveLength(1);
177
- });
178
-
179
- it("skips add-message commands with no supported parts", async () => {
180
- const fetchMock = installFetch();
181
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
182
- const { aui, sendCommand } = mountRuntime();
183
- await waitFor(() =>
184
- expect(
185
- (aui().thread.getState().extras as { sendCommand?: unknown })
186
- ?.sendCommand,
187
- ).toBeTypeOf("function"),
188
- );
189
-
190
- act(() =>
191
- aui().thread.append({
192
- role: "user",
193
- content: [{ type: "audio", audio: { data: "", format: "mp3" } }],
194
- }),
195
- );
196
-
197
- await act(async () => {});
198
- expect(warn).toHaveBeenCalledWith(
199
- "[assistant-ui] Skipped add-message command with no supported parts",
200
- );
201
- expect(fetchMock.requests).toHaveLength(0);
202
-
203
- // The skipped message must not leak its parentId into later batches.
204
- act(() => sendCommand(createMessageCommand("follow-up")));
205
- await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
206
- expect(fetchMock.requests[0]!.body).not.toHaveProperty("parentId");
207
- });
208
-
209
- it("flushes commands enqueued during a resume run in a follow-up run", async () => {
210
- const fetchMock = installFetch();
211
- const { aui, sendCommand } = mountRuntime({
212
- resumeApi: "https://example.com/resume",
213
- });
214
- await waitFor(() =>
215
- expect(
216
- (aui().thread.getState().extras as { sendCommand?: unknown })
217
- ?.sendCommand,
218
- ).toBeTypeOf("function"),
219
- );
220
-
221
- act(() => sendCommand(createMessageCommand("a")));
222
- await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
223
-
224
- // Both land while the first run is active and coalesce into one follow-up.
225
- act(() => {
226
- sendCommand(createMessageCommand("b"));
227
- aui().thread.resumeRun({ parentId: null });
228
- });
229
-
230
- act(() => fetchMock.servers[0]!.close());
231
- await waitFor(() => expect(fetchMock.requests).toHaveLength(2));
232
- expect(fetchMock.requests[1]!.url).toBe("https://example.com/resume");
233
- expect(fetchMock.requests[1]!.body["commands"]).toEqual([]);
234
- expect(fetchMock.requests[1]!.body).toHaveProperty("state");
235
-
236
- // "b" coalesced into the resume run and must not starve in the queue.
237
- act(() => fetchMock.servers[1]!.close());
238
- await waitFor(() => expect(fetchMock.requests).toHaveLength(3));
239
- expect(fetchMock.requests[2]!.url).toBe("https://example.com/api");
240
- expect(fetchMock.requests[2]!.body["commands"]).toEqual([
241
- createMessageCommand("b"),
242
- ]);
243
-
244
- act(() => fetchMock.servers[2]!.close());
245
- await waitFor(() => expect(aui().thread.getState().isRunning).toBe(false));
246
- });
247
-
248
- it.each(["error", "cancellation"] as const)(
249
- "does not apply a dropped resume after run %s",
250
- async (settlement) => {
251
- const fetchMock = installPendingFetch();
252
- const onError = vi.fn();
253
- const { aui, sendCommand } = mountRuntime({
254
- resumeApi: "https://example.com/resume",
255
- onError,
256
- });
257
- await waitFor(() =>
258
- expect(
259
- (aui().thread.getState().extras as { sendCommand?: unknown })
260
- ?.sendCommand,
261
- ).toBeTypeOf("function"),
262
- );
263
-
264
- act(() => sendCommand(createMessageCommand("a")));
265
- await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
266
-
267
- await act(async () => {
268
- await aui().thread.resumeRun({ parentId: null });
269
- });
270
- if (settlement === "cancellation") {
271
- act(() => aui().thread.cancelRun());
272
- } else {
273
- await act(async () => {
274
- fetchMock.pending[0]!.reject(new Error("request failed"));
275
- });
276
- }
277
- await waitFor(() =>
278
- expect(aui().thread.getState().isRunning).toBe(false),
279
- );
280
-
281
- act(() => sendCommand(createMessageCommand("b")));
282
- await waitFor(() => expect(fetchMock.requests).toHaveLength(2));
283
- expect(fetchMock.requests[1]!.url).toBe("https://example.com/api");
284
- expect(fetchMock.requests[1]!.body["commands"]).toEqual([
285
- createMessageCommand("b"),
286
- ]);
287
-
288
- await act(async () => {
289
- fetchMock.pending[1]!.resolve(new Response("", { status: 200 }));
290
- });
291
- await waitFor(() =>
292
- expect(aui().thread.getState().isRunning).toBe(false),
293
- );
294
- expect(onError).toHaveBeenCalledTimes(settlement === "error" ? 1 : 0);
295
- },
296
- );
297
-
298
- it("applies resumed operations to the retained initial state", async () => {
299
- const requests: RecordedRequest[] = [];
300
- vi.stubGlobal(
301
- "fetch",
302
- async (url: RequestInfo | URL, init: RequestInit = {}) => {
303
- requests.push({
304
- url: String(url),
305
- init,
306
- body: JSON.parse(init.body as string),
307
- });
308
-
309
- if (String(url) === "https://example.com/resume-state") {
310
- return Response.json({
311
- runId: "run-1",
312
- state: { message: "Hello" },
313
- });
314
- }
315
-
316
- return new Response(
317
- 'aui-state:[{"type":"append-text","path":["message"],"value":" world"}]\n',
318
- { status: 200 },
319
- );
320
- },
321
- );
322
- const { aui } = mountRuntime({
323
- resumeApi: "https://example.com/resume",
324
- resumeStateApi: "https://example.com/resume-state",
325
- });
326
- await waitFor(() =>
327
- expect(
328
- (aui().thread.getState().extras as { sendCommand?: unknown })
329
- ?.sendCommand,
330
- ).toBeTypeOf("function"),
331
- );
332
-
333
- act(() => {
334
- aui().thread.importExternalState({ message: "Wrong" });
335
- });
336
- await act(async () => {
337
- await aui().thread.resumeRun({ parentId: null });
338
- });
339
-
340
- await waitFor(() =>
341
- expect(
342
- (aui().thread.getState().extras as { state: unknown }).state,
343
- ).toEqual({ message: "Hello world" }),
344
- );
345
- expect(requests.map((request) => request.url)).toEqual([
346
- "https://example.com/resume-state",
347
- "https://example.com/resume",
348
- ]);
349
- expect(requests[1]!.body).toMatchObject({ runId: "run-1" });
350
- expect(requests[1]!.body).not.toHaveProperty("state");
351
- });
352
-
353
- it("rejects malformed resume state responses before replay", async () => {
354
- const fetchMock = vi.fn(async () => Response.json({ state: {} }));
355
- vi.stubGlobal("fetch", fetchMock);
356
- const onError = vi.fn();
357
- const { aui } = mountRuntime({
358
- resumeApi: "https://example.com/resume",
359
- resumeStateApi: "https://example.com/resume-state",
360
- onError,
361
- });
362
- await waitFor(() =>
363
- expect(
364
- (aui().thread.getState().extras as { sendCommand?: unknown })
365
- ?.sendCommand,
366
- ).toBeTypeOf("function"),
367
- );
368
-
369
- await act(async () => {
370
- await aui().thread.resumeRun({ parentId: null });
371
- });
372
-
373
- await waitFor(() =>
374
- expect(onError).toHaveBeenCalledWith(
375
- expect.objectContaining({
376
- message: "Resume state response must contain state and runId",
377
- }),
378
- expect.anything(),
379
- ),
380
- );
381
- expect(fetchMock).toHaveBeenCalledTimes(1);
382
- });
383
-
384
- it("commits a retained null state locally and omits state from the resume request", async () => {
385
- const requests: RecordedRequest[] = [];
386
- vi.stubGlobal(
387
- "fetch",
388
- async (url: RequestInfo | URL, init: RequestInit = {}) => {
389
- requests.push({
390
- url: String(url),
391
- init,
392
- body: JSON.parse(init.body as string),
393
- });
394
-
395
- if (String(url) === "https://example.com/resume-state") {
396
- return Response.json({ runId: "run-1", state: null });
397
- }
398
-
399
- return new Response("", { status: 200 });
400
- },
401
- );
402
- const { aui } = mountRuntime({
403
- resumeApi: "https://example.com/resume",
404
- resumeStateApi: "https://example.com/resume-state",
405
- });
406
- await waitFor(() =>
407
- expect(
408
- (aui().thread.getState().extras as { sendCommand?: unknown })
409
- ?.sendCommand,
410
- ).toBeTypeOf("function"),
411
- );
412
-
413
- act(() => {
414
- aui().thread.importExternalState({ message: "Wrong" });
415
- });
416
- await act(async () => {
417
- await aui().thread.resumeRun({ parentId: null });
418
- });
419
-
420
- expect(requests[1]!.body).toMatchObject({ runId: "run-1" });
421
- expect(requests[1]!.body).not.toHaveProperty("state");
422
- await waitFor(() =>
423
- expect(
424
- (aui().thread.getState().extras as { state: unknown }).state,
425
- ).toBeNull(),
426
- );
427
- });
428
-
429
- it("skips the resume without error when the state endpoint reports no active run", async () => {
430
- const fetchMock = vi.fn(async () => new Response(null, { status: 204 }));
431
- vi.stubGlobal("fetch", fetchMock);
432
- const onError = vi.fn();
433
- const { aui } = mountRuntime({
434
- resumeApi: "https://example.com/resume",
435
- resumeStateApi: "https://example.com/resume-state",
436
- onError,
437
- });
438
- await waitFor(() =>
439
- expect(
440
- (aui().thread.getState().extras as { sendCommand?: unknown })
441
- ?.sendCommand,
442
- ).toBeTypeOf("function"),
443
- );
444
-
445
- act(() => {
446
- aui().thread.importExternalState({ message: "Kept" });
447
- });
448
- await act(async () => {
449
- await aui().thread.resumeRun({ parentId: null });
450
- });
451
-
452
- await waitFor(() => expect(aui().thread.getState().isRunning).toBe(false));
453
- expect(fetchMock).toHaveBeenCalledTimes(1);
454
- expect(onError).not.toHaveBeenCalled();
455
- expect(
456
- (aui().thread.getState().extras as { state: unknown }).state,
457
- ).toEqual({ message: "Kept" });
458
- });
459
-
460
- it("keeps the retained runId over body overrides in the resume request", async () => {
461
- const requests: RecordedRequest[] = [];
462
- vi.stubGlobal(
463
- "fetch",
464
- async (url: RequestInfo | URL, init: RequestInit = {}) => {
465
- requests.push({
466
- url: String(url),
467
- init,
468
- body: JSON.parse(init.body as string),
469
- });
470
-
471
- if (String(url) === "https://example.com/resume-state") {
472
- return Response.json({
473
- runId: "run-1",
474
- state: { message: "Hello" },
475
- });
476
- }
477
-
478
- return new Response("", { status: 200 });
479
- },
480
- );
481
- const { aui } = mountRuntime({
482
- resumeApi: "https://example.com/resume",
483
- resumeStateApi: "https://example.com/resume-state",
484
- body: { state: { message: "Injected" }, runId: "bogus" },
485
- });
486
- await waitFor(() =>
487
- expect(
488
- (aui().thread.getState().extras as { sendCommand?: unknown })
489
- ?.sendCommand,
490
- ).toBeTypeOf("function"),
491
- );
492
-
493
- await act(async () => {
494
- await aui().thread.resumeRun({ parentId: null });
495
- });
496
-
497
- expect(requests[1]!.body["runId"]).toBe("run-1");
498
- expect(requests[1]!.body).not.toHaveProperty("state");
499
- });
500
-
501
- it("re-attaches runId and strips substituted state when prepareSendCommandsRequest rebuilds the body", async () => {
502
- const requests: RecordedRequest[] = [];
503
- vi.stubGlobal(
504
- "fetch",
505
- async (url: RequestInfo | URL, init: RequestInit = {}) => {
506
- requests.push({
507
- url: String(url),
508
- init,
509
- body: JSON.parse(init.body as string),
510
- });
511
-
512
- if (String(url) === "https://example.com/resume-state") {
513
- return Response.json({
514
- runId: "run-1",
515
- state: { message: "Hello" },
516
- });
517
- }
518
-
519
- return new Response("", { status: 200 });
520
- },
521
- );
522
- const { aui } = mountRuntime({
523
- resumeApi: "https://example.com/resume",
524
- resumeStateApi: "https://example.com/resume-state",
525
- prepareSendCommandsRequest: (body) => ({
526
- commands: body.commands,
527
- state: { message: "Substituted" },
528
- rebuilt: true,
529
- }),
530
- });
531
- await waitFor(() =>
532
- expect(
533
- (aui().thread.getState().extras as { sendCommand?: unknown })
534
- ?.sendCommand,
535
- ).toBeTypeOf("function"),
536
- );
537
-
538
- await act(async () => {
539
- await aui().thread.resumeRun({ parentId: null });
540
- });
541
-
542
- expect(requests[1]!.body).toMatchObject({ runId: "run-1", rebuilt: true });
543
- expect(requests[1]!.body).not.toHaveProperty("state");
544
- });
545
-
546
- it("keeps local state when the matching resume stream is rejected", async () => {
547
- const fetchMock = vi
548
- .fn()
549
- .mockResolvedValueOnce(
550
- Response.json({ runId: "run-1", state: { message: "Hello" } }),
551
- )
552
- .mockResolvedValueOnce(new Response("run mismatch", { status: 409 }));
553
- vi.stubGlobal("fetch", fetchMock);
554
- const onError = vi.fn();
555
- const { aui } = mountRuntime({
556
- resumeApi: "https://example.com/resume",
557
- resumeStateApi: "https://example.com/resume-state",
558
- onError,
559
- });
560
- await waitFor(() =>
561
- expect(
562
- (aui().thread.getState().extras as { sendCommand?: unknown })
563
- ?.sendCommand,
564
- ).toBeTypeOf("function"),
565
- );
566
-
567
- act(() => {
568
- aui().thread.importExternalState({ message: "Wrong" });
569
- });
570
- await act(async () => {
571
- await aui().thread.resumeRun({ parentId: null });
572
- });
573
-
574
- await waitFor(() =>
575
- expect(onError).toHaveBeenCalledWith(
576
- expect.objectContaining({ message: "Status 409: run mismatch" }),
577
- expect.anything(),
578
- ),
579
- );
580
- expect(
581
- (aui().thread.getState().extras as { state: unknown }).state,
582
- ).toEqual({ message: "Wrong" });
583
- expect(fetchMock).toHaveBeenCalledTimes(2);
584
- });
585
- });