@assistant-ui/core 0.3.18 → 0.3.19
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.
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +13 -3
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/thread-history.d.ts +4 -0
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +26 -11
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +11 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.d.ts.map +1 -1
- package/dist/react/client/DataRenderers.js +20 -15
- package/dist/react/client/DataRenderers.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +11 -5
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +3 -2
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +3 -2
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
- package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractable.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js +4 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.js +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.d.ts +1 -0
- package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
- package/dist/react/primitives/part/PartMessages.js +1 -0
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js +6 -5
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +62 -15
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
- package/dist/react/runtimes/external-message-converter.d.ts +12 -2
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +26 -61
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/utils/groupParts.d.ts +6 -1
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +6 -0
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/react/utils/useJSONEqual.d.ts +3 -1
- package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
- package/dist/react/utils/useJSONEqual.js +6 -1
- package/dist/react/utils/useJSONEqual.js.map +1 -1
- package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
- package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
- package/dist/react/utils/useJSONSchemaDependency.js +20 -0
- package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +2 -5
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +46 -10
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +155 -64
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.d.ts +5 -0
- package/dist/runtime/queue/message-queue.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +61 -6
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtime/utils/auto-status.d.ts +6 -2
- package/dist/runtime/utils/auto-status.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.js +12 -4
- package/dist/runtime/utils/auto-status.js.map +1 -1
- package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
- package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +48 -12
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.js +3 -3
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +2 -1
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -2
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +241 -200
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/thread-tasks.d.ts +13 -0
- package/dist/store/clients/thread-tasks.d.ts.map +1 -0
- package/dist/store/clients/thread-tasks.js +69 -0
- package/dist/store/clients/thread-tasks.js.map +1 -0
- package/dist/store/index.d.ts +3 -2
- package/dist/store/index.js +3 -3
- package/dist/store/primitive-predicates.d.ts.map +1 -1
- package/dist/store/primitive-predicates.js +4 -4
- package/dist/store/primitive-predicates.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -0
- package/dist/store/scope-registration.d.ts.map +1 -1
- package/dist/store/scopes/task.d.ts +36 -0
- package/dist/store/scopes/task.d.ts.map +1 -0
- package/dist/store/scopes/task.js +0 -0
- package/dist/store/scopes/thread.d.ts +20 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +2 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +23 -15
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +8 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts.map +1 -1
- package/dist/utils/abortable-iterable.js +30 -14
- package/dist/utils/abortable-iterable.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +18 -2
- package/dist/utils/getGroupStatus.d.ts.map +1 -1
- package/dist/utils/getGroupStatus.js +32 -7
- package/dist/utils/getGroupStatus.js.map +1 -1
- package/dist/utils/wire-media.d.ts.map +1 -1
- package/dist/utils/wire-media.js +2 -2
- package/dist/utils/wire-media.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/speech.test.ts +236 -0
- package/src/adapters/speech.ts +17 -10
- package/src/adapters/thread-history.ts +3 -0
- package/src/index.ts +1 -0
- package/src/model-context/frame/host.test.ts +113 -1
- package/src/model-context/frame/host.ts +39 -12
- package/src/model-context/interactable-composer-metadata.test.ts +30 -0
- package/src/model-context/interactable-composer-metadata.ts +10 -10
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
- package/src/react/client/DataRenderers.test.tsx +45 -0
- package/src/react/client/DataRenderers.ts +13 -9
- package/src/react/client/Interactables.test.ts +44 -1
- package/src/react/client/Interactables.ts +7 -1
- package/src/react/client/RemoteThreadList.test.ts +41 -1
- package/src/react/client/RemoteThreadList.ts +15 -2
- package/src/react/client/interactable-model-context.test.ts +44 -0
- package/src/react/client/interactable-model-context.ts +3 -2
- package/src/react/index.ts +5 -1
- package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
- package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
- package/src/react/model-context/useInteractable.test.tsx +177 -0
- package/src/react/model-context/useInteractable.ts +3 -0
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
- package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
- package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
- package/src/react/primitives/part/PartMessages.test.tsx +104 -1
- package/src/react/primitives/part/PartMessages.tsx +1 -0
- package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
- package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
- package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
- package/src/react/runtimes/cloud/auiV0.ts +160 -25
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
- package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
- package/src/react/runtimes/external-message-converter.test.ts +136 -1
- package/src/react/runtimes/external-message-converter.ts +39 -93
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
- package/src/react/utils/groupParts.ts +12 -1
- package/src/react/utils/useJSONEqual.ts +9 -0
- package/src/react/utils/useJSONSchemaDependency.ts +22 -0
- package/src/runtime/api/thread-runtime.test.ts +58 -0
- package/src/runtime/api/thread-runtime.ts +2 -4
- package/src/runtime/base/base-composer-runtime-core.ts +69 -16
- package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
- package/src/runtime/base/base-thread-runtime-core.ts +222 -83
- package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
- package/src/runtime/queue/message-queue.ts +68 -6
- package/src/runtime/utils/auto-status.test.ts +112 -1
- package/src/runtime/utils/auto-status.ts +31 -5
- package/src/runtime/utils/external-message-conversion.test.ts +134 -0
- package/src/runtime/utils/external-message-conversion.ts +122 -31
- package/src/runtime/utils/message-repository.test.ts +49 -0
- package/src/runtime/utils/message-repository.ts +3 -3
- package/src/runtime/utils/thread-message-like.test.ts +52 -0
- package/src/runtime/utils/thread-message-like.ts +4 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
- package/src/store/clients/external-thread.ts +19 -1
- package/src/store/clients/thread-tasks.test.ts +274 -0
- package/src/store/clients/thread-tasks.ts +138 -0
- package/src/store/index.ts +8 -0
- package/src/store/primitive-predicates.test.ts +48 -5
- package/src/store/primitive-predicates.ts +7 -2
- package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
- package/src/store/scope-registration.ts +2 -0
- package/src/store/scopes/task.ts +34 -0
- package/src/store/scopes/thread.ts +16 -0
- package/src/store/thread-tasks.test.tsx +209 -0
- package/src/subscribable/subscribable.test.ts +156 -1
- package/src/subscribable/subscribable.ts +25 -15
- package/src/tests/MessageRepository.test.ts +1 -5
- package/src/tests/auiV0Encode.test.ts +300 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
- package/src/tests/base-composer-runtime-core.test.ts +314 -2
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
- package/src/tests/external-thread-attachments.test.tsx +69 -0
- package/src/tests/external-thread-parity.test.tsx +34 -0
- package/src/tests/groupParts.test.ts +71 -0
- package/src/tests/message-queue.test.ts +253 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +8 -0
- package/src/utils/abortable-iterable.test.ts +115 -0
- package/src/utils/abortable-iterable.ts +34 -23
- package/src/utils/getGroupStatus.test.ts +70 -4
- package/src/utils/getGroupStatus.ts +35 -12
- package/src/utils/wire-media.test.ts +10 -0
- package/src/utils/wire-media.ts +6 -2
|
@@ -114,6 +114,53 @@ describe("groupPartByType", () => {
|
|
|
114
114
|
]);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("routes a tool call by name through its tool-call:<name> entry", () => {
|
|
118
|
+
const fn = groupPartByType({
|
|
119
|
+
"tool-call:task": ["group-subagents"],
|
|
120
|
+
"tool-call": ["group-tool"],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(
|
|
124
|
+
fn(
|
|
125
|
+
part({
|
|
126
|
+
type: "tool-call",
|
|
127
|
+
toolName: "task",
|
|
128
|
+
} as Partial<PartState>),
|
|
129
|
+
),
|
|
130
|
+
).toEqual(["group-subagents"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("falls back to the plain tool-call entry for other tool names", () => {
|
|
134
|
+
const fn = groupPartByType({
|
|
135
|
+
"tool-call:task": ["group-subagents"],
|
|
136
|
+
"tool-call": ["group-tool"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(
|
|
140
|
+
fn(
|
|
141
|
+
part({
|
|
142
|
+
type: "tool-call",
|
|
143
|
+
toolName: "search",
|
|
144
|
+
} as Partial<PartState>),
|
|
145
|
+
),
|
|
146
|
+
).toEqual(["group-tool"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("routes standalone tool calls before matching a tool name entry", () => {
|
|
150
|
+
const fn = groupPartByType({
|
|
151
|
+
"standalone-tool-call": ["group-standalone"],
|
|
152
|
+
"tool-call:task": ["group-subagents"],
|
|
153
|
+
"tool-call": ["group-tool"],
|
|
154
|
+
});
|
|
155
|
+
const standaloneTask = part({
|
|
156
|
+
type: "tool-call",
|
|
157
|
+
toolName: "task",
|
|
158
|
+
mcp: { app: { resourceUri: "ui://task" } },
|
|
159
|
+
} as Partial<PartState>);
|
|
160
|
+
|
|
161
|
+
expect(fn(standaloneTask)).toEqual(["group-standalone"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
117
164
|
it("returns [] for part types not in the map", () => {
|
|
118
165
|
const fn = groupPartByType({ reasoning: ["group-r"] });
|
|
119
166
|
expect(fn(part({ type: "text" }))).toEqual([]);
|
|
@@ -254,6 +301,30 @@ describe("groupPartByType", () => {
|
|
|
254
301
|
];
|
|
255
302
|
expect(keyA).not.toBe(keyB);
|
|
256
303
|
});
|
|
304
|
+
|
|
305
|
+
it("includes tool-call name entries in an order-insensitive fingerprint", () => {
|
|
306
|
+
const withoutName = groupPartByType({ "tool-call": ["group-tool"] });
|
|
307
|
+
const a = groupPartByType({
|
|
308
|
+
"tool-call:task": ["group-subagents"],
|
|
309
|
+
"tool-call": ["group-tool"],
|
|
310
|
+
});
|
|
311
|
+
const b = groupPartByType({
|
|
312
|
+
"tool-call": ["group-tool"],
|
|
313
|
+
"tool-call:task": ["group-subagents"],
|
|
314
|
+
});
|
|
315
|
+
const withoutNameKey = (
|
|
316
|
+
withoutName as unknown as { [GROUPBY_MEMO_KEY]: string }
|
|
317
|
+
)[GROUPBY_MEMO_KEY];
|
|
318
|
+
const keyA = (a as unknown as { [GROUPBY_MEMO_KEY]: string })[
|
|
319
|
+
GROUPBY_MEMO_KEY
|
|
320
|
+
];
|
|
321
|
+
const keyB = (b as unknown as { [GROUPBY_MEMO_KEY]: string })[
|
|
322
|
+
GROUPBY_MEMO_KEY
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
expect(keyA).not.toBe(withoutNameKey);
|
|
326
|
+
expect(keyA).toBe(keyB);
|
|
327
|
+
});
|
|
257
328
|
});
|
|
258
329
|
|
|
259
330
|
describe("buildGroupTree idKey", () => {
|
|
@@ -435,6 +435,259 @@ describe("createMessageQueue", () => {
|
|
|
435
435
|
});
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
+
describe("synchronous dispatch failures", () => {
|
|
439
|
+
it("restores a message when the driver throws", () => {
|
|
440
|
+
const error = new Error("dispatch failed");
|
|
441
|
+
const run = vi.fn(() => {
|
|
442
|
+
throw error;
|
|
443
|
+
});
|
|
444
|
+
const { adapter } = createMessageQueue({ run });
|
|
445
|
+
|
|
446
|
+
expect(() => adapter.enqueue(msg("first"))).toThrow(error);
|
|
447
|
+
expect(prompts(adapter.items)).toEqual(["first"]);
|
|
448
|
+
|
|
449
|
+
run.mockImplementation(() => undefined);
|
|
450
|
+
adapter.enqueue(msg("second"));
|
|
451
|
+
|
|
452
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
453
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
454
|
+
expect.objectContaining({ content: [{ type: "text", text: "first" }] }),
|
|
455
|
+
{ steer: false },
|
|
456
|
+
);
|
|
457
|
+
expect(prompts(adapter.items)).toEqual(["second"]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("does not restore work after the driver starts its run", () => {
|
|
461
|
+
const error = new Error("dispatch failed");
|
|
462
|
+
let fail = true;
|
|
463
|
+
let controller!: ReturnType<typeof createMessageQueue>;
|
|
464
|
+
const run = vi.fn(() => {
|
|
465
|
+
if (!fail) return;
|
|
466
|
+
controller.notifyBusy();
|
|
467
|
+
throw error;
|
|
468
|
+
});
|
|
469
|
+
controller = createMessageQueue({ run });
|
|
470
|
+
|
|
471
|
+
expect(() => controller.adapter.enqueue(msg("first"))).toThrow(error);
|
|
472
|
+
expect(prompts(controller.adapter.items)).toEqual([]);
|
|
473
|
+
|
|
474
|
+
fail = false;
|
|
475
|
+
controller.adapter.enqueue(msg("second"));
|
|
476
|
+
expect(run).toHaveBeenCalledOnce();
|
|
477
|
+
|
|
478
|
+
controller.notifyIdle();
|
|
479
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
480
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
481
|
+
expect.objectContaining({
|
|
482
|
+
content: [{ type: "text", text: "second" }],
|
|
483
|
+
}),
|
|
484
|
+
{ steer: false },
|
|
485
|
+
);
|
|
486
|
+
expect(prompts(controller.adapter.items)).toEqual([]);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("restores a message when its dispatch transform throws", () => {
|
|
490
|
+
const error = new Error("transform failed");
|
|
491
|
+
const run = vi.fn();
|
|
492
|
+
const { adapter } = createMessageQueue({ run });
|
|
493
|
+
adapter.__internal_setDispatchTransform(() => {
|
|
494
|
+
throw error;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(() => adapter.enqueue(msg("first"))).toThrow(error);
|
|
498
|
+
expect(run).not.toHaveBeenCalled();
|
|
499
|
+
expect(prompts(adapter.items)).toEqual(["first"]);
|
|
500
|
+
|
|
501
|
+
adapter.__internal_setDispatchTransform((message) => message);
|
|
502
|
+
adapter.enqueue(msg("second"));
|
|
503
|
+
|
|
504
|
+
expect(run).toHaveBeenCalledWith(
|
|
505
|
+
expect.objectContaining({ content: [{ type: "text", text: "first" }] }),
|
|
506
|
+
{ steer: false },
|
|
507
|
+
);
|
|
508
|
+
expect(prompts(adapter.items)).toEqual(["second"]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("restores a steer when cancellation throws", () => {
|
|
512
|
+
const error = new Error("cancel failed");
|
|
513
|
+
const run = vi.fn();
|
|
514
|
+
const cancel = vi.fn(() => {
|
|
515
|
+
throw error;
|
|
516
|
+
});
|
|
517
|
+
const { adapter, notifyIdle } = createMessageQueue({ run, cancel });
|
|
518
|
+
|
|
519
|
+
adapter.enqueue(msg("active"));
|
|
520
|
+
expect(() => adapter.steer(msg("urgent"))).toThrow(error);
|
|
521
|
+
expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
|
|
522
|
+
|
|
523
|
+
notifyIdle();
|
|
524
|
+
expect(run).toHaveBeenCalledOnce();
|
|
525
|
+
|
|
526
|
+
cancel.mockImplementation(() => undefined);
|
|
527
|
+
adapter.enqueue(msg("later"));
|
|
528
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
529
|
+
expect.objectContaining({
|
|
530
|
+
content: [{ type: "text", text: "urgent" }],
|
|
531
|
+
}),
|
|
532
|
+
{ steer: false },
|
|
533
|
+
);
|
|
534
|
+
expect(prompts(adapter.items)).toEqual(["later"]);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("restores a steer when its dispatch transform throws", () => {
|
|
538
|
+
const error = new Error("transform failed");
|
|
539
|
+
const run = vi.fn();
|
|
540
|
+
const cancel = vi.fn();
|
|
541
|
+
const { adapter, notifyIdle } = createMessageQueue({ run, cancel });
|
|
542
|
+
|
|
543
|
+
adapter.enqueue(msg("active"));
|
|
544
|
+
adapter.__internal_setDispatchTransform(() => {
|
|
545
|
+
throw error;
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
expect(() => adapter.steer(msg("urgent"))).toThrow(error);
|
|
549
|
+
expect(cancel).toHaveBeenCalledOnce();
|
|
550
|
+
expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
|
|
551
|
+
|
|
552
|
+
notifyIdle();
|
|
553
|
+
adapter.__internal_setDispatchTransform((message) => message);
|
|
554
|
+
adapter.enqueue(msg("later"));
|
|
555
|
+
|
|
556
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
557
|
+
expect.objectContaining({
|
|
558
|
+
content: [{ type: "text", text: "urgent" }],
|
|
559
|
+
}),
|
|
560
|
+
{ steer: false },
|
|
561
|
+
);
|
|
562
|
+
expect(prompts(adapter.items)).toEqual(["later"]);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("restores a steer when its replacement run throws", () => {
|
|
566
|
+
const error = new Error("steer failed");
|
|
567
|
+
let failSteer = true;
|
|
568
|
+
const run = vi.fn(
|
|
569
|
+
(_message: AppendMessage, options: { steer: boolean }) => {
|
|
570
|
+
if (options.steer && failSteer) throw error;
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
const { adapter, notifyIdle } = createMessageQueue({
|
|
574
|
+
run,
|
|
575
|
+
cancel: vi.fn(),
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
adapter.enqueue(msg("active"));
|
|
579
|
+
expect(() => adapter.steer(msg("urgent"))).toThrow(error);
|
|
580
|
+
expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
|
|
581
|
+
|
|
582
|
+
failSteer = false;
|
|
583
|
+
notifyIdle();
|
|
584
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
585
|
+
|
|
586
|
+
adapter.enqueue(msg("later"));
|
|
587
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
588
|
+
expect.objectContaining({
|
|
589
|
+
content: [{ type: "text", text: "urgent" }],
|
|
590
|
+
}),
|
|
591
|
+
{ steer: false },
|
|
592
|
+
);
|
|
593
|
+
expect(prompts(adapter.items)).toEqual(["later"]);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("recovers when cancellation settles before the replacement throws", () => {
|
|
597
|
+
const error = new Error("steer failed");
|
|
598
|
+
let failSteer = true;
|
|
599
|
+
let controller!: ReturnType<typeof createMessageQueue>;
|
|
600
|
+
controller = createMessageQueue({
|
|
601
|
+
run: (_message, options) => {
|
|
602
|
+
if (options.steer && failSteer) throw error;
|
|
603
|
+
},
|
|
604
|
+
cancel: () => controller.notifyIdle(),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
controller.adapter.enqueue(msg("active"));
|
|
608
|
+
expect(() => controller.adapter.steer(msg("urgent"))).toThrow(error);
|
|
609
|
+
expect(prompts(controller.adapter.steerItems)).toEqual(["urgent"]);
|
|
610
|
+
|
|
611
|
+
failSteer = false;
|
|
612
|
+
controller.adapter.enqueue(msg("later"));
|
|
613
|
+
|
|
614
|
+
expect(prompts(controller.adapter.steerItems)).toEqual([]);
|
|
615
|
+
expect(prompts(controller.adapter.items)).toEqual(["later"]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("does not restore a steer after its replacement run starts", () => {
|
|
619
|
+
const error = new Error("steer failed");
|
|
620
|
+
let failSteer = true;
|
|
621
|
+
let controller!: ReturnType<typeof createMessageQueue>;
|
|
622
|
+
const run = vi.fn(
|
|
623
|
+
(_message: AppendMessage, options: { steer: boolean }) => {
|
|
624
|
+
if (!options.steer || !failSteer) return;
|
|
625
|
+
controller.notifyBusy();
|
|
626
|
+
throw error;
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
controller = createMessageQueue({
|
|
630
|
+
run,
|
|
631
|
+
cancel: () => controller.notifyIdle(),
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
controller.adapter.enqueue(msg("active"));
|
|
635
|
+
expect(() => controller.adapter.steer(msg("urgent"))).toThrow(error);
|
|
636
|
+
expect(prompts(controller.adapter.steerItems)).toEqual([]);
|
|
637
|
+
|
|
638
|
+
failSteer = false;
|
|
639
|
+
controller.adapter.enqueue(msg("later"));
|
|
640
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
641
|
+
|
|
642
|
+
controller.notifyIdle();
|
|
643
|
+
expect(run).toHaveBeenCalledTimes(3);
|
|
644
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
645
|
+
expect.objectContaining({
|
|
646
|
+
content: [{ type: "text", text: "later" }],
|
|
647
|
+
}),
|
|
648
|
+
{ steer: false },
|
|
649
|
+
);
|
|
650
|
+
expect(prompts(controller.adapter.items)).toEqual([]);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("restores an unanchored move when its replacement run throws", () => {
|
|
654
|
+
const error = new Error("steer failed");
|
|
655
|
+
let failSteer = true;
|
|
656
|
+
const run = vi.fn(
|
|
657
|
+
(_message: AppendMessage, options: { steer: boolean }) => {
|
|
658
|
+
if (options.steer && failSteer) throw error;
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
const { adapter, notifyIdle } = createMessageQueue({
|
|
662
|
+
run,
|
|
663
|
+
cancel: vi.fn(),
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
adapter.enqueue(msg("active"));
|
|
667
|
+
adapter.enqueue(msg("first"));
|
|
668
|
+
adapter.enqueue(msg("second"));
|
|
669
|
+
adapter.enqueue(msg("moved"));
|
|
670
|
+
const movedId = adapter.items[2]!.id;
|
|
671
|
+
|
|
672
|
+
expect(() => adapter.move(movedId, { lane: "steer" })).toThrow(error);
|
|
673
|
+
expect(adapter.steerItems).toHaveLength(0);
|
|
674
|
+
expect(prompts(adapter.items)).toEqual(["first", "second", "moved"]);
|
|
675
|
+
expect(adapter.items[2]?.id).toBe(movedId);
|
|
676
|
+
|
|
677
|
+
failSteer = false;
|
|
678
|
+
notifyIdle();
|
|
679
|
+
adapter.enqueue(msg("later"));
|
|
680
|
+
|
|
681
|
+
expect(run).toHaveBeenLastCalledWith(
|
|
682
|
+
expect.objectContaining({
|
|
683
|
+
content: [{ type: "text", text: "first" }],
|
|
684
|
+
}),
|
|
685
|
+
{ steer: false },
|
|
686
|
+
);
|
|
687
|
+
expect(prompts(adapter.items)).toEqual(["second", "moved", "later"]);
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
438
691
|
it("notifyCancelled keeps items and pauses advance until the next send", () => {
|
|
439
692
|
const run = vi.fn();
|
|
440
693
|
const { adapter, notifyIdle, notifyCancelled } = createMessageQueue({
|
|
@@ -88,4 +88,55 @@ describe("onThreadIdChange", () => {
|
|
|
88
88
|
expect(emitted).not.toBe(localId);
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it("does not reject a completed switch when the callback throws", async () => {
|
|
93
|
+
const callbackError = new Error("host callback failed");
|
|
94
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
95
|
+
try {
|
|
96
|
+
const cb = vi.fn(() => {
|
|
97
|
+
throw callbackError;
|
|
98
|
+
});
|
|
99
|
+
const { core } = createCoreWithCallback(cb);
|
|
100
|
+
await flush();
|
|
101
|
+
|
|
102
|
+
await expect(core.switchToThread("existing-1")).resolves.toBeUndefined();
|
|
103
|
+
|
|
104
|
+
expect(core.mainThreadId).toBe("existing-1");
|
|
105
|
+
expect(cb).toHaveBeenCalledExactlyOnceWith("existing-1");
|
|
106
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
107
|
+
"[assistant-ui] onThreadIdChange callback threw an error",
|
|
108
|
+
callbackError,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await expect(core.switchToThread("existing-2")).resolves.toBeUndefined();
|
|
112
|
+
expect(core.mainThreadId).toBe("existing-2");
|
|
113
|
+
expect(cb).toHaveBeenLastCalledWith("existing-2");
|
|
114
|
+
} finally {
|
|
115
|
+
errorSpy.mockRestore();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("contains a rejected callback promise", async () => {
|
|
120
|
+
const callbackError = new Error("async host callback failed");
|
|
121
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
122
|
+
try {
|
|
123
|
+
const cb = vi.fn(async () => {
|
|
124
|
+
throw callbackError;
|
|
125
|
+
});
|
|
126
|
+
const { core } = createCoreWithCallback(cb);
|
|
127
|
+
await flush();
|
|
128
|
+
|
|
129
|
+
await expect(core.switchToThread("existing-1")).resolves.toBeUndefined();
|
|
130
|
+
await vi.waitFor(() => {
|
|
131
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
132
|
+
"[assistant-ui] onThreadIdChange callback threw an error",
|
|
133
|
+
callbackError,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(core.mainThreadId).toBe("existing-1");
|
|
138
|
+
} finally {
|
|
139
|
+
errorSpy.mockRestore();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
91
142
|
});
|
|
@@ -269,6 +269,49 @@ describe("useRemoteThreadListRuntime controlled threadId", () => {
|
|
|
269
269
|
expect(onThreadIdChange).toHaveBeenLastCalledWith(undefined);
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
it("contains rejected callback promises on runtime-initiated switches", async () => {
|
|
273
|
+
const callbackError = new Error("async host callback failed");
|
|
274
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
275
|
+
const adapter = makeAdapter();
|
|
276
|
+
const onThreadIdChange = vi.fn(async () => {
|
|
277
|
+
throw callbackError;
|
|
278
|
+
});
|
|
279
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
280
|
+
|
|
281
|
+
const { unmount } = render(
|
|
282
|
+
<ControlledRuntime
|
|
283
|
+
adapter={adapter}
|
|
284
|
+
threadId="thread-a"
|
|
285
|
+
onThreadIdChange={onThreadIdChange}
|
|
286
|
+
runtimeRef={runtimeRef}
|
|
287
|
+
/>,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await waitForRemoteThread(runtimeRef, "thread-a");
|
|
292
|
+
|
|
293
|
+
await act(async () => {
|
|
294
|
+
await expect(
|
|
295
|
+
runtimeRef.current!.threads.switchToThread("thread-b"),
|
|
296
|
+
).resolves.toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
301
|
+
"[assistant-ui] onThreadIdChange callback threw an error",
|
|
302
|
+
callbackError,
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith("thread-b");
|
|
306
|
+
expect(runtimeRef.current!.threads.mainItem.getState().remoteId).toBe(
|
|
307
|
+
"thread-b",
|
|
308
|
+
);
|
|
309
|
+
} finally {
|
|
310
|
+
unmount();
|
|
311
|
+
errorSpy.mockRestore();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
272
315
|
it("does not retain suppression after an initial switch fails", async () => {
|
|
273
316
|
let threadAFetchCount = 0;
|
|
274
317
|
const adapter = makeAdapter({
|
package/src/types/index.ts
CHANGED
package/src/types/message.ts
CHANGED
|
@@ -395,6 +395,8 @@ export type MessageTiming = {
|
|
|
395
395
|
readonly toolCallCount: number;
|
|
396
396
|
};
|
|
397
397
|
|
|
398
|
+
export type MessageModality = "voice";
|
|
399
|
+
|
|
398
400
|
export type ThreadStep = {
|
|
399
401
|
readonly messageId?: string;
|
|
400
402
|
readonly usage?:
|
|
@@ -420,6 +422,7 @@ export type ThreadSystemMessage = MessageCommonProps & {
|
|
|
420
422
|
readonly steps?: undefined;
|
|
421
423
|
readonly submittedFeedback?: undefined;
|
|
422
424
|
readonly timing?: undefined;
|
|
425
|
+
readonly modality?: undefined;
|
|
423
426
|
readonly custom: Record<string, unknown>;
|
|
424
427
|
};
|
|
425
428
|
};
|
|
@@ -436,6 +439,8 @@ export type ThreadUserMessage = MessageCommonProps & {
|
|
|
436
439
|
readonly submittedFeedback?: undefined;
|
|
437
440
|
readonly timing?: undefined;
|
|
438
441
|
readonly isOptimistic?: boolean;
|
|
442
|
+
/** Set when the message was produced by a voice session rather than typed. */
|
|
443
|
+
readonly modality?: MessageModality;
|
|
439
444
|
readonly custom: Record<string, unknown>;
|
|
440
445
|
};
|
|
441
446
|
};
|
|
@@ -456,6 +461,8 @@ export type ThreadAssistantMessage = MessageCommonProps & {
|
|
|
456
461
|
* once off the head branch and are never persisted.
|
|
457
462
|
*/
|
|
458
463
|
readonly isOptimistic?: boolean;
|
|
464
|
+
/** Set when the message was produced by a voice session rather than typed. */
|
|
465
|
+
readonly modality?: MessageModality;
|
|
459
466
|
readonly custom: Record<string, unknown>;
|
|
460
467
|
};
|
|
461
468
|
};
|
|
@@ -470,6 +477,7 @@ type BaseThreadMessage = {
|
|
|
470
477
|
readonly submittedFeedback?: { readonly type: "positive" | "negative" };
|
|
471
478
|
readonly timing?: MessageTiming;
|
|
472
479
|
readonly isOptimistic?: boolean;
|
|
480
|
+
readonly modality?: MessageModality;
|
|
473
481
|
readonly custom: Record<string, unknown>;
|
|
474
482
|
};
|
|
475
483
|
readonly attachments?: ThreadUserMessage["attachments"];
|
|
@@ -16,6 +16,121 @@ const collect = async <T>(iterable: AsyncIterable<T>) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
describe("abortableIterable", () => {
|
|
19
|
+
it.each([false, true])(
|
|
20
|
+
"releases the opening abort listener on rejection=%s",
|
|
21
|
+
async (fails) => {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const added = vi.spyOn(controller.signal, "addEventListener");
|
|
24
|
+
const removed = vi.spyOn(controller.signal, "removeEventListener");
|
|
25
|
+
const source: AsyncIterable<number> = {
|
|
26
|
+
async *[Symbol.asyncIterator]() {
|
|
27
|
+
yield 1;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const opening = openAbortableIterable(
|
|
31
|
+
fails ? Promise.reject(new Error("opening failed")) : source,
|
|
32
|
+
controller.signal,
|
|
33
|
+
);
|
|
34
|
+
if (fails) await expect(opening).rejects.toThrow("opening failed");
|
|
35
|
+
else await expect(opening).resolves.toBe(source);
|
|
36
|
+
expect(added).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(removed).toHaveBeenCalledWith("abort", added.mock.calls[0]![1]);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
it("finalizes once when an aborted read later rejects", async () => {
|
|
42
|
+
let rejectRead!: (error: unknown) => void;
|
|
43
|
+
const read = new Promise<IteratorResult<number>>((_, reject) => {
|
|
44
|
+
rejectRead = reject;
|
|
45
|
+
});
|
|
46
|
+
const finalize = vi.fn(async () => ({
|
|
47
|
+
done: true as const,
|
|
48
|
+
value: undefined,
|
|
49
|
+
}));
|
|
50
|
+
const source: AsyncIterable<number> = {
|
|
51
|
+
[Symbol.asyncIterator]: () => ({ next: () => read, return: finalize }),
|
|
52
|
+
};
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const iterator = abortableIterable(source, controller.signal)[
|
|
55
|
+
Symbol.asyncIterator
|
|
56
|
+
]();
|
|
57
|
+
const pending = iterator.next();
|
|
58
|
+
controller.abort();
|
|
59
|
+
await expect(pending).resolves.toEqual({ done: true, value: undefined });
|
|
60
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
61
|
+
rejectRead(new Error("late failure"));
|
|
62
|
+
await read.catch(() => {});
|
|
63
|
+
await iterator.return?.();
|
|
64
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("releases the listener and finalizes after a synchronous read failure", async () => {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const added = vi.spyOn(controller.signal, "addEventListener");
|
|
70
|
+
const removed = vi.spyOn(controller.signal, "removeEventListener");
|
|
71
|
+
const error = new Error("read failed");
|
|
72
|
+
const finalize = vi.fn(async () => ({
|
|
73
|
+
done: true as const,
|
|
74
|
+
value: undefined,
|
|
75
|
+
}));
|
|
76
|
+
const source: AsyncIterable<number> = {
|
|
77
|
+
[Symbol.asyncIterator]: () => ({
|
|
78
|
+
next: () => {
|
|
79
|
+
throw error;
|
|
80
|
+
},
|
|
81
|
+
return: finalize,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
await expect(
|
|
85
|
+
collect(abortableIterable(source, controller.signal)),
|
|
86
|
+
).rejects.toBe(error);
|
|
87
|
+
expect(removed).toHaveBeenCalledWith("abort", added.mock.calls[0]![1]);
|
|
88
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
89
|
+
controller.abort();
|
|
90
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not read or finalize again after the consumer returns", async () => {
|
|
94
|
+
const next = vi.fn(async () => ({ done: false as const, value: 1 }));
|
|
95
|
+
const finalize = vi.fn(async () => ({
|
|
96
|
+
done: true as const,
|
|
97
|
+
value: undefined,
|
|
98
|
+
}));
|
|
99
|
+
const source: AsyncIterable<number> = {
|
|
100
|
+
[Symbol.asyncIterator]: () => ({ next, return: finalize }),
|
|
101
|
+
};
|
|
102
|
+
const iterator = abortableIterable(source, new AbortController().signal)[
|
|
103
|
+
Symbol.asyncIterator
|
|
104
|
+
]();
|
|
105
|
+
await iterator.return?.();
|
|
106
|
+
await iterator.return?.();
|
|
107
|
+
await expect(iterator.next()).resolves.toEqual({
|
|
108
|
+
done: true,
|
|
109
|
+
value: undefined,
|
|
110
|
+
});
|
|
111
|
+
expect(next).not.toHaveBeenCalled();
|
|
112
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does not finalize a naturally exhausted source", async () => {
|
|
116
|
+
const next = vi.fn(async () => ({ done: true as const, value: undefined }));
|
|
117
|
+
const finalize = vi.fn(async () => ({
|
|
118
|
+
done: true as const,
|
|
119
|
+
value: undefined,
|
|
120
|
+
}));
|
|
121
|
+
const source: AsyncIterable<number> = {
|
|
122
|
+
[Symbol.asyncIterator]: () => ({ next, return: finalize }),
|
|
123
|
+
};
|
|
124
|
+
const iterator = abortableIterable(source, new AbortController().signal)[
|
|
125
|
+
Symbol.asyncIterator
|
|
126
|
+
]();
|
|
127
|
+
await iterator.next();
|
|
128
|
+
await iterator.next();
|
|
129
|
+
await iterator.return?.();
|
|
130
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
19
134
|
it("settles an opening stream on abort and finalizes a late iterable", async () => {
|
|
20
135
|
const opened = deferred<AsyncIterable<number>>();
|
|
21
136
|
const finalize = vi.fn(async () => ({
|