@assistant-ui/core 0.2.22 → 0.2.23
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/thread-history.d.ts +8 -0
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.js +3 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +36 -21
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +13 -4
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -0
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/interactable-model-context.js +1 -0
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/interactables-legacy/useInteractableState.js +1 -0
- package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractableState.js +1 -0
- package/dist/react/model-context/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.js +1 -0
- package/dist/react/model-context/useInteractableVersions.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +1 -0
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +1 -0
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
- package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
- package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
- package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
- package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
- package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
- package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
- package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
- package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
- package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.js +1 -0
- package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
- package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
- package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
- package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
- package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
- package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageReload.js +1 -0
- package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
- package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
- package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +1 -0
- package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -4
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +11 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +2 -1
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts +0 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/assistant-runtime.js +1 -1
- package/dist/runtime/api/assistant-runtime.js.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.js +1 -1
- package/dist/runtime/api/attachment-runtime.js.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts +2 -2
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.js +2 -2
- package/dist/runtime/api/composer-runtime.js.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts +4 -4
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.js +3 -3
- package/dist/runtime/api/message-part-runtime.js.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +2 -2
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +2 -2
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +1 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +2 -2
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +4 -2
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.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 +12 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +2 -1
- package/dist/runtimes/internal.js +2 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +44 -13
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/no-op-composer-client.js +1 -0
- package/dist/store/clients/no-op-composer-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
- package/dist/store/runtime-clients/thread-list-runtime-client.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 +17 -15
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread.d.ts +1 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +3 -3
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +3 -3
- package/dist/subscribable/subscribable.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/thread-history.ts +8 -0
- package/src/internal.ts +1 -1
- package/src/model-context/frame/host.test.ts +47 -0
- package/src/model-context/frame/host.ts +46 -19
- package/src/model-context/types.ts +16 -6
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +15 -5
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +20 -6
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +26 -5
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +5 -1
- package/src/react/runtimes/cloud/auiV0.ts +14 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
- package/src/react/runtimes/external-message-converter.test.ts +346 -0
- package/src/react/runtimes/external-message-converter.ts +1 -1
- package/src/react/runtimes/useLocalRuntime.test.tsx +43 -0
- package/src/runtime/api/assistant-runtime.ts +4 -1
- package/src/runtime/api/attachment-runtime.ts +8 -2
- package/src/runtime/api/composer-runtime.ts +9 -5
- package/src/runtime/api/message-part-runtime.ts +10 -3
- package/src/runtime/api/message-runtime.ts +7 -2
- package/src/runtime/api/thread-list-item-runtime.ts +7 -2
- package/src/runtime/api/thread-list-runtime.ts +10 -2
- package/src/runtime/base/base-thread-runtime-core.ts +5 -1
- package/src/runtime/base/default-edit-composer-runtime-core.ts +14 -2
- package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
- package/src/runtime/utils/message-repository.ts +19 -13
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
- package/src/runtimes/internal.ts +3 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +596 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +85 -21
- package/src/runtimes/local/should-continue.test.ts +122 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +9 -10
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +38 -32
- package/src/store/clients/model-context-client.test.ts +21 -1
- package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
- package/src/store/scopes/thread.ts +1 -0
- package/src/subscribable/subscribable.ts +16 -8
- package/src/tests/MessageRepository.test.ts +12 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
- package/src/tests/auiV0Encode.test.ts +74 -0
- package/src/tests/base-composer-runtime-core.test.ts +422 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
- package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
RemoteThreadListResponse,
|
|
4
|
+
RemoteThreadMetadata,
|
|
5
|
+
} from "../runtimes/remote-thread-list/types";
|
|
6
|
+
import {
|
|
7
|
+
createCore,
|
|
8
|
+
deferred,
|
|
9
|
+
makeAdapter,
|
|
10
|
+
} from "./remote-thread-list-test-helpers";
|
|
11
|
+
|
|
12
|
+
const thread = (id: string): RemoteThreadMetadata => ({
|
|
13
|
+
status: "regular",
|
|
14
|
+
remoteId: id,
|
|
15
|
+
externalId: id,
|
|
16
|
+
title: id,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const archivedThread = (id: string): RemoteThreadMetadata => ({
|
|
20
|
+
status: "archived",
|
|
21
|
+
remoteId: id,
|
|
22
|
+
externalId: id,
|
|
23
|
+
title: id,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("RemoteThreadListThreadListRuntimeCore.switchToThread position", () => {
|
|
27
|
+
it("keeps the position a concurrent list() already gave the thread", async () => {
|
|
28
|
+
const TARGET = "thread-target";
|
|
29
|
+
const listDeferred = deferred<RemoteThreadListResponse>();
|
|
30
|
+
const fetchDeferred = deferred<RemoteThreadMetadata>();
|
|
31
|
+
|
|
32
|
+
const adapter = makeAdapter({
|
|
33
|
+
list: vi.fn(() => listDeferred.promise),
|
|
34
|
+
fetch: vi.fn(() => fetchDeferred.promise),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const core = createCore(adapter, TARGET);
|
|
38
|
+
|
|
39
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
40
|
+
const switchPromise = core.switchToThread(TARGET);
|
|
41
|
+
|
|
42
|
+
// `list()` resolves first and places TARGET at the top, as the most recent.
|
|
43
|
+
listDeferred.resolve({
|
|
44
|
+
threads: [thread(TARGET), thread("thread-a"), thread("thread-b")],
|
|
45
|
+
});
|
|
46
|
+
await loadPromise;
|
|
47
|
+
|
|
48
|
+
expect(core.threadIds).toEqual([TARGET, "thread-a", "thread-b"]);
|
|
49
|
+
|
|
50
|
+
fetchDeferred.resolve(thread(TARGET));
|
|
51
|
+
await switchPromise;
|
|
52
|
+
|
|
53
|
+
// The fetch path must not filter TARGET out of position and re-append it.
|
|
54
|
+
expect(core.threadIds).toEqual([TARGET, "thread-a", "thread-b"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("appends a thread that is genuinely absent from the loaded list", async () => {
|
|
58
|
+
const TARGET = "thread-target";
|
|
59
|
+
const listDeferred = deferred<RemoteThreadListResponse>();
|
|
60
|
+
const fetchDeferred = deferred<RemoteThreadMetadata>();
|
|
61
|
+
|
|
62
|
+
const adapter = makeAdapter({
|
|
63
|
+
list: vi.fn(() => listDeferred.promise),
|
|
64
|
+
fetch: vi.fn(() => fetchDeferred.promise),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const core = createCore(adapter, TARGET);
|
|
68
|
+
|
|
69
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
70
|
+
const switchPromise = core.switchToThread(TARGET);
|
|
71
|
+
|
|
72
|
+
// `list()` resolves first but does not include TARGET (e.g. it is past the
|
|
73
|
+
// first page).
|
|
74
|
+
listDeferred.resolve({
|
|
75
|
+
threads: [thread("thread-a"), thread("thread-b")],
|
|
76
|
+
});
|
|
77
|
+
await loadPromise;
|
|
78
|
+
|
|
79
|
+
expect(core.threadIds).toEqual(["thread-a", "thread-b"]);
|
|
80
|
+
|
|
81
|
+
fetchDeferred.resolve(thread(TARGET));
|
|
82
|
+
await switchPromise;
|
|
83
|
+
|
|
84
|
+
// An absent thread may live on an unloaded page; appending keeps it below
|
|
85
|
+
// every loaded (newer) thread instead of pinning it to the top.
|
|
86
|
+
expect(core.threadIds).toEqual(["thread-a", "thread-b", TARGET]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps the position a concurrent list() gave an archived thread", async () => {
|
|
90
|
+
const TARGET = "thread-target";
|
|
91
|
+
const listDeferred = deferred<RemoteThreadListResponse>();
|
|
92
|
+
const fetchDeferred = deferred<RemoteThreadMetadata>();
|
|
93
|
+
|
|
94
|
+
const adapter = makeAdapter({
|
|
95
|
+
list: vi.fn(() => listDeferred.promise),
|
|
96
|
+
fetch: vi.fn(() => fetchDeferred.promise),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const core = createCore(adapter, TARGET);
|
|
100
|
+
// Stub unarchive so auto-unarchive doesn't move the thread into `threadIds`
|
|
101
|
+
// via `updateStatusReducer`, hiding the position written by the fetch path.
|
|
102
|
+
(
|
|
103
|
+
core as unknown as { unarchive: (id: string) => Promise<void> }
|
|
104
|
+
).unarchive = async () => {};
|
|
105
|
+
|
|
106
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
107
|
+
const switchPromise = core.switchToThread(TARGET);
|
|
108
|
+
|
|
109
|
+
listDeferred.resolve({
|
|
110
|
+
threads: [archivedThread(TARGET), archivedThread("thread-a")],
|
|
111
|
+
});
|
|
112
|
+
await loadPromise;
|
|
113
|
+
|
|
114
|
+
expect(core.archivedThreadIds).toEqual([TARGET, "thread-a"]);
|
|
115
|
+
|
|
116
|
+
fetchDeferred.resolve(archivedThread(TARGET));
|
|
117
|
+
await switchPromise;
|
|
118
|
+
|
|
119
|
+
expect(core.archivedThreadIds).toEqual([TARGET, "thread-a"]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -53,6 +53,35 @@ describe("auiV0Encode", () => {
|
|
|
53
53
|
]);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it("preserves a pending tool-call approval in the core cloud encoder", () => {
|
|
57
|
+
const encoded = auiV0Encode({
|
|
58
|
+
id: "m1",
|
|
59
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
60
|
+
role: "assistant",
|
|
61
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
62
|
+
metadata: {
|
|
63
|
+
unstable_state: null,
|
|
64
|
+
unstable_annotations: [],
|
|
65
|
+
unstable_data: [],
|
|
66
|
+
steps: [],
|
|
67
|
+
custom: {},
|
|
68
|
+
},
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "tool-call",
|
|
72
|
+
toolCallId: "call-1",
|
|
73
|
+
toolName: "send_email",
|
|
74
|
+
args: {},
|
|
75
|
+
argsText: "{}",
|
|
76
|
+
approval: { id: "a1" },
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const toolCall = encoded.content.find((p) => p.type === "tool-call");
|
|
82
|
+
expect(toolCall).toMatchObject({ approval: { id: "a1" } });
|
|
83
|
+
});
|
|
84
|
+
|
|
56
85
|
it("preserves user attachments in the core cloud encoder", () => {
|
|
57
86
|
const encoded = auiV0Encode({
|
|
58
87
|
id: "m1",
|
|
@@ -129,6 +158,51 @@ describe("auiV0Encode", () => {
|
|
|
129
158
|
});
|
|
130
159
|
|
|
131
160
|
describe("auiV0Decode", () => {
|
|
161
|
+
it("round-trips a pending tool-call approval so a paused run stays resumable", () => {
|
|
162
|
+
const content = auiV0Encode({
|
|
163
|
+
id: "local",
|
|
164
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
165
|
+
role: "assistant",
|
|
166
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
167
|
+
metadata: {
|
|
168
|
+
unstable_state: null,
|
|
169
|
+
unstable_annotations: [],
|
|
170
|
+
unstable_data: [],
|
|
171
|
+
steps: [],
|
|
172
|
+
custom: {},
|
|
173
|
+
},
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "tool-call",
|
|
177
|
+
toolCallId: "call-1",
|
|
178
|
+
toolName: "send_email",
|
|
179
|
+
args: {},
|
|
180
|
+
argsText: "{}",
|
|
181
|
+
approval: { id: "a1" },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const decoded = auiV0Decode({
|
|
187
|
+
id: "cloud",
|
|
188
|
+
parent_id: null,
|
|
189
|
+
height: 0,
|
|
190
|
+
created_at: new Date("2026-03-15T00:00:00.000Z"),
|
|
191
|
+
updated_at: new Date("2026-03-15T00:00:00.000Z"),
|
|
192
|
+
format: "aui/v0",
|
|
193
|
+
content: content as never,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (decoded.message.role !== "assistant")
|
|
197
|
+
throw new Error("expected assistant");
|
|
198
|
+
const toolCall = decoded.message.content.find(
|
|
199
|
+
(p) => p.type === "tool-call",
|
|
200
|
+
);
|
|
201
|
+
expect(toolCall?.type === "tool-call" && toolCall.approval).toEqual({
|
|
202
|
+
id: "a1",
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
132
206
|
it("restores user attachments from core cloud history", () => {
|
|
133
207
|
const content = auiV0Encode({
|
|
134
208
|
id: "local",
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { BaseComposerRuntimeCore } from "../runtime/base/base-composer-runtime-core";
|
|
3
|
+
import type { AttachmentAdapter } from "../adapters/attachment";
|
|
4
|
+
import type { DictationAdapter } from "../adapters/speech";
|
|
5
|
+
import type { CreateAttachment, PendingAttachment } from "../types/attachment";
|
|
6
|
+
import type { AppendMessage } from "../types/message";
|
|
7
|
+
import type { SendOptions } from "../runtime/interfaces/composer-runtime-core";
|
|
8
|
+
|
|
9
|
+
class TestComposerCore extends BaseComposerRuntimeCore {
|
|
10
|
+
private _attachmentAdapter: AttachmentAdapter | undefined;
|
|
11
|
+
public sentMessages: Array<Omit<AppendMessage, "parentId" | "sourceId">> = [];
|
|
12
|
+
public sentOptions: Array<SendOptions | undefined> = [];
|
|
13
|
+
|
|
14
|
+
protected getAttachmentAdapter() {
|
|
15
|
+
return this._attachmentAdapter;
|
|
16
|
+
}
|
|
17
|
+
protected getDictationAdapter(): DictationAdapter | undefined {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setAttachmentAdapter(adapter: AttachmentAdapter | undefined) {
|
|
22
|
+
this._attachmentAdapter = adapter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get canCancel() {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get canSend() {
|
|
30
|
+
return !this.isEmpty;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected handleSend(
|
|
34
|
+
message: Omit<AppendMessage, "parentId" | "sourceId">,
|
|
35
|
+
options?: SendOptions,
|
|
36
|
+
) {
|
|
37
|
+
this.sentMessages.push(message);
|
|
38
|
+
this.sentOptions.push(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected handleCancel() {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const makePendingAttachment = (
|
|
45
|
+
id: string,
|
|
46
|
+
name = "file.txt",
|
|
47
|
+
): PendingAttachment => ({
|
|
48
|
+
id,
|
|
49
|
+
type: "file",
|
|
50
|
+
name,
|
|
51
|
+
contentType: "text/plain",
|
|
52
|
+
file: new File(["content"], name),
|
|
53
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("BaseComposerRuntimeCore", () => {
|
|
57
|
+
let composer: TestComposerCore;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
composer = new TestComposerCore();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not notify when a set value is unchanged", () => {
|
|
64
|
+
composer.setText("same");
|
|
65
|
+
const listener = vi.fn();
|
|
66
|
+
composer.subscribe(listener);
|
|
67
|
+
|
|
68
|
+
composer.setText("same");
|
|
69
|
+
expect(listener).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("isEmpty returns true for whitespace-only text", () => {
|
|
73
|
+
composer.setText(" ");
|
|
74
|
+
expect(composer.isEmpty).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reset clears text, attachments, role, runConfig, and quote", async () => {
|
|
78
|
+
composer.setText("hello");
|
|
79
|
+
composer.setRole("assistant");
|
|
80
|
+
composer.setRunConfig({ custom: { k: "v" } });
|
|
81
|
+
composer.setQuote({ text: "q", messageId: "m1" });
|
|
82
|
+
|
|
83
|
+
await composer.reset();
|
|
84
|
+
|
|
85
|
+
expect(composer.text).toBe("");
|
|
86
|
+
expect(composer.role).toBe("user");
|
|
87
|
+
expect(composer.runConfig).toEqual({});
|
|
88
|
+
expect(composer.quote).toBeUndefined();
|
|
89
|
+
expect(composer.attachments).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reset does not notify when already in default state", async () => {
|
|
93
|
+
const listener = vi.fn();
|
|
94
|
+
composer.subscribe(listener);
|
|
95
|
+
|
|
96
|
+
await composer.reset();
|
|
97
|
+
expect(listener).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("send includes quote in metadata and clears it", async () => {
|
|
101
|
+
const quote = { text: "quoted", messageId: "m1" };
|
|
102
|
+
composer.setText("reply");
|
|
103
|
+
composer.setQuote(quote);
|
|
104
|
+
|
|
105
|
+
await composer.send();
|
|
106
|
+
|
|
107
|
+
const msg = composer.sentMessages[0]!;
|
|
108
|
+
expect(msg.metadata).toEqual({ custom: { quote } });
|
|
109
|
+
expect(composer.quote).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("forwards send options to handleSend", async () => {
|
|
113
|
+
composer.setText("a");
|
|
114
|
+
await composer.send();
|
|
115
|
+
composer.setText("b");
|
|
116
|
+
await composer.send({ startRun: true });
|
|
117
|
+
composer.setText("c");
|
|
118
|
+
await composer.send({ startRun: false });
|
|
119
|
+
|
|
120
|
+
expect(composer.sentOptions).toEqual([
|
|
121
|
+
undefined,
|
|
122
|
+
{ startRun: true },
|
|
123
|
+
{ startRun: false },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("removeAttachment removes via adapter", async () => {
|
|
128
|
+
const pending = makePendingAttachment("att-1");
|
|
129
|
+
const adapter: AttachmentAdapter = {
|
|
130
|
+
accept: "*",
|
|
131
|
+
add: vi.fn().mockResolvedValue(pending),
|
|
132
|
+
remove: vi.fn().mockResolvedValue(undefined),
|
|
133
|
+
send: vi.fn(),
|
|
134
|
+
};
|
|
135
|
+
composer.setAttachmentAdapter(adapter);
|
|
136
|
+
|
|
137
|
+
await composer.addAttachment(new File([""], "f.txt"));
|
|
138
|
+
expect(composer.attachments).toHaveLength(1);
|
|
139
|
+
|
|
140
|
+
await composer.removeAttachment("att-1");
|
|
141
|
+
expect(composer.attachments).toHaveLength(0);
|
|
142
|
+
expect(adapter.remove).toHaveBeenCalledWith(pending);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("removeAttachment throws for unknown id", async () => {
|
|
146
|
+
const adapter: AttachmentAdapter = {
|
|
147
|
+
accept: "*",
|
|
148
|
+
add: vi.fn(),
|
|
149
|
+
remove: vi.fn(),
|
|
150
|
+
send: vi.fn(),
|
|
151
|
+
};
|
|
152
|
+
composer.setAttachmentAdapter(adapter);
|
|
153
|
+
|
|
154
|
+
await expect(composer.removeAttachment("nonexistent")).rejects.toThrow(
|
|
155
|
+
"Attachment not found",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("unstable_on unsubscribe stops notifications", async () => {
|
|
160
|
+
const callback = vi.fn();
|
|
161
|
+
const unsub = composer.unstable_on("send", callback);
|
|
162
|
+
unsub();
|
|
163
|
+
|
|
164
|
+
composer.setText("test");
|
|
165
|
+
await composer.send();
|
|
166
|
+
|
|
167
|
+
expect(callback).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("attachmentAccept returns adapter accept or default", () => {
|
|
171
|
+
expect(composer.attachmentAccept).toBe("*");
|
|
172
|
+
|
|
173
|
+
const adapter: AttachmentAdapter = {
|
|
174
|
+
accept: "image/*",
|
|
175
|
+
add: vi.fn(),
|
|
176
|
+
remove: vi.fn(),
|
|
177
|
+
send: vi.fn(),
|
|
178
|
+
};
|
|
179
|
+
composer.setAttachmentAdapter(adapter);
|
|
180
|
+
expect(composer.attachmentAccept).toBe("image/*");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("CreateAttachment (external source)", () => {
|
|
184
|
+
const makeCreateAttachment = (
|
|
185
|
+
overrides?: Partial<CreateAttachment>,
|
|
186
|
+
): CreateAttachment => ({
|
|
187
|
+
name: "external-doc.pdf",
|
|
188
|
+
content: [{ type: "text", text: "extracted content" }],
|
|
189
|
+
...overrides,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("addAttachment with CreateAttachment adds without adapter", async () => {
|
|
193
|
+
const att = makeCreateAttachment();
|
|
194
|
+
await composer.addAttachment(att);
|
|
195
|
+
|
|
196
|
+
expect(composer.attachments).toHaveLength(1);
|
|
197
|
+
expect(composer.attachments[0]!.name).toBe("external-doc.pdf");
|
|
198
|
+
expect(composer.attachments[0]!.status).toEqual({ type: "complete" });
|
|
199
|
+
expect(composer.attachments[0]!.type).toBe("document");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("addAttachment with CreateAttachment uses provided id and type", async () => {
|
|
203
|
+
const att = makeCreateAttachment({
|
|
204
|
+
id: "custom-id",
|
|
205
|
+
type: "image",
|
|
206
|
+
contentType: "image/png",
|
|
207
|
+
});
|
|
208
|
+
await composer.addAttachment(att);
|
|
209
|
+
|
|
210
|
+
expect(composer.attachments[0]!.id).toBe("custom-id");
|
|
211
|
+
expect(composer.attachments[0]!.type).toBe("image");
|
|
212
|
+
expect(composer.attachments[0]!.contentType).toBe("image/png");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("addAttachment with CreateAttachment generates id when not provided", async () => {
|
|
216
|
+
const att = makeCreateAttachment();
|
|
217
|
+
await composer.addAttachment(att);
|
|
218
|
+
|
|
219
|
+
expect(composer.attachments[0]!.id).toBeTruthy();
|
|
220
|
+
expect(typeof composer.attachments[0]!.id).toBe("string");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("addAttachment with CreateAttachment fires attachmentAdd event", async () => {
|
|
224
|
+
const callback = vi.fn();
|
|
225
|
+
composer.unstable_on("attachmentAdd", callback);
|
|
226
|
+
|
|
227
|
+
await composer.addAttachment(makeCreateAttachment());
|
|
228
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("removeAttachment on complete attachment does not require adapter", async () => {
|
|
232
|
+
await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
|
|
233
|
+
expect(composer.attachments).toHaveLength(1);
|
|
234
|
+
|
|
235
|
+
await composer.removeAttachment("ext-1");
|
|
236
|
+
expect(composer.attachments).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("isEmpty returns false when external attachment present", async () => {
|
|
240
|
+
expect(composer.isEmpty).toBe(true);
|
|
241
|
+
|
|
242
|
+
await composer.addAttachment(makeCreateAttachment());
|
|
243
|
+
expect(composer.isEmpty).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("send with mixed pending + complete attachments", async () => {
|
|
247
|
+
const pending = makePendingAttachment("att-pending");
|
|
248
|
+
const completedFromAdapter = {
|
|
249
|
+
id: "att-pending",
|
|
250
|
+
type: "file" as const,
|
|
251
|
+
name: "file.txt",
|
|
252
|
+
contentType: "text/plain",
|
|
253
|
+
content: [{ type: "text" as const, text: "file content" }],
|
|
254
|
+
status: { type: "complete" as const },
|
|
255
|
+
};
|
|
256
|
+
const adapter: AttachmentAdapter = {
|
|
257
|
+
accept: "*",
|
|
258
|
+
add: vi.fn().mockResolvedValue(pending),
|
|
259
|
+
remove: vi.fn(),
|
|
260
|
+
send: vi.fn().mockResolvedValue(completedFromAdapter),
|
|
261
|
+
};
|
|
262
|
+
composer.setAttachmentAdapter(adapter);
|
|
263
|
+
|
|
264
|
+
await composer.addAttachment(new File(["data"], "file.txt"));
|
|
265
|
+
await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
|
|
266
|
+
|
|
267
|
+
expect(composer.attachments).toHaveLength(2);
|
|
268
|
+
|
|
269
|
+
composer.setText("hello");
|
|
270
|
+
await composer.send();
|
|
271
|
+
|
|
272
|
+
expect(composer.sentMessages).toHaveLength(1);
|
|
273
|
+
const msg = composer.sentMessages[0]!;
|
|
274
|
+
expect(msg.attachments).toHaveLength(2);
|
|
275
|
+
expect(adapter.send).toHaveBeenCalledTimes(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("adapter.accept enforcement", () => {
|
|
279
|
+
const makeImageAdapter = (): AttachmentAdapter => ({
|
|
280
|
+
accept: "image/*",
|
|
281
|
+
add: vi.fn(),
|
|
282
|
+
remove: vi.fn(),
|
|
283
|
+
send: vi.fn(),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("rejects external attachment with contentType not matching adapter.accept", async () => {
|
|
287
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
288
|
+
const onError = vi.fn();
|
|
289
|
+
const onAdd = vi.fn();
|
|
290
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
291
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
292
|
+
|
|
293
|
+
await expect(
|
|
294
|
+
composer.addAttachment(
|
|
295
|
+
makeCreateAttachment({
|
|
296
|
+
name: "doc.pdf",
|
|
297
|
+
contentType: "application/pdf",
|
|
298
|
+
}),
|
|
299
|
+
),
|
|
300
|
+
).rejects.toThrow(/File type application\/pdf is not accepted/);
|
|
301
|
+
|
|
302
|
+
expect(composer.attachments).toHaveLength(0);
|
|
303
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(onError).toHaveBeenCalledWith(
|
|
305
|
+
expect.objectContaining({
|
|
306
|
+
reason: "not-accepted",
|
|
307
|
+
message: expect.stringContaining(
|
|
308
|
+
"File type application/pdf is not accepted",
|
|
309
|
+
),
|
|
310
|
+
error: expect.any(Error),
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("accepts external attachment with contentType matching adapter.accept", async () => {
|
|
317
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
318
|
+
const onError = vi.fn();
|
|
319
|
+
|
|
320
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
321
|
+
await composer.addAttachment(
|
|
322
|
+
makeCreateAttachment({
|
|
323
|
+
name: "photo.png",
|
|
324
|
+
contentType: "image/png",
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(composer.attachments).toHaveLength(1);
|
|
329
|
+
expect(onError).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("accepts external attachment when only filename extension matches adapter.accept", async () => {
|
|
333
|
+
composer.setAttachmentAdapter({
|
|
334
|
+
accept: ".pdf",
|
|
335
|
+
add: vi.fn(),
|
|
336
|
+
remove: vi.fn(),
|
|
337
|
+
send: vi.fn(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await composer.addAttachment(
|
|
341
|
+
makeCreateAttachment({ name: "report.pdf" }),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(composer.attachments).toHaveLength(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("rejects external attachment when contentType missing and extension does not match", async () => {
|
|
348
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
349
|
+
const onError = vi.fn();
|
|
350
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
351
|
+
|
|
352
|
+
await expect(
|
|
353
|
+
composer.addAttachment(makeCreateAttachment({ name: "notes.txt" })),
|
|
354
|
+
).rejects.toThrow(/is not accepted/);
|
|
355
|
+
|
|
356
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("adds external attachment without check when no adapter is configured", async () => {
|
|
360
|
+
await composer.addAttachment(
|
|
361
|
+
makeCreateAttachment({
|
|
362
|
+
name: "anything.xyz",
|
|
363
|
+
contentType: "application/x-anything",
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(composer.attachments).toHaveLength(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("isolates a throwing attachmentAdd subscriber without firing attachmentAddError", async () => {
|
|
371
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
372
|
+
const listenerError = new Error("add subscriber boom");
|
|
373
|
+
const consoleError = vi
|
|
374
|
+
.spyOn(console, "error")
|
|
375
|
+
.mockImplementation(() => {});
|
|
376
|
+
const onError = vi.fn();
|
|
377
|
+
composer.unstable_on("attachmentAdd", () => {
|
|
378
|
+
throw listenerError;
|
|
379
|
+
});
|
|
380
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
381
|
+
|
|
382
|
+
await expect(
|
|
383
|
+
composer.addAttachment(
|
|
384
|
+
makeCreateAttachment({
|
|
385
|
+
name: "photo.png",
|
|
386
|
+
contentType: "image/png",
|
|
387
|
+
}),
|
|
388
|
+
),
|
|
389
|
+
).resolves.toBeUndefined();
|
|
390
|
+
|
|
391
|
+
expect(composer.attachments).toHaveLength(1);
|
|
392
|
+
expect(onError).not.toHaveBeenCalled();
|
|
393
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
394
|
+
'[assistant-ui] Composer runtime "attachmentAdd" listener threw an error',
|
|
395
|
+
listenerError,
|
|
396
|
+
);
|
|
397
|
+
consoleError.mockRestore();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("does not fire attachmentAddError when a state subscriber throws on add", async () => {
|
|
401
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
402
|
+
const onError = vi.fn();
|
|
403
|
+
composer.subscribe(() => {
|
|
404
|
+
throw new Error("state subscriber boom");
|
|
405
|
+
});
|
|
406
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
407
|
+
|
|
408
|
+
await expect(
|
|
409
|
+
composer.addAttachment(
|
|
410
|
+
makeCreateAttachment({
|
|
411
|
+
name: "photo.png",
|
|
412
|
+
contentType: "image/png",
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
415
|
+
).rejects.toThrow("state subscriber boom");
|
|
416
|
+
|
|
417
|
+
expect(composer.attachments).toHaveLength(1);
|
|
418
|
+
expect(onError).not.toHaveBeenCalled();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -27,7 +27,7 @@ describe("ExternalStoreThreadListRuntimeCore - construction", () => {
|
|
|
27
27
|
expect(core.getItemById(core.mainThreadId)).toBeDefined();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it("picks up mainThreadId from adapter.threadId on construction
|
|
30
|
+
it("picks up mainThreadId from adapter.threadId on construction", () => {
|
|
31
31
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
32
32
|
makeAdapter({ threadId: "thread-alpha" }),
|
|
33
33
|
makeFactory(),
|
|
@@ -147,7 +147,7 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
|
|
|
147
147
|
expect(callback).toHaveBeenCalled();
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
it("synthesizes mainThreadId entry after a switch to a threadId not in the threads list
|
|
150
|
+
it("synthesizes mainThreadId entry after a switch to a threadId not in the threads list", () => {
|
|
151
151
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
152
152
|
makeAdapter({ threadId: "thread-alpha" }),
|
|
153
153
|
makeFactory(),
|
|
@@ -158,7 +158,7 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
|
|
|
158
158
|
expect(item?.id).toBe("thread-beta");
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it("does not retain stale synthesized entries across mainThreadId switches
|
|
161
|
+
it("does not retain stale synthesized entries across mainThreadId switches", () => {
|
|
162
162
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
163
163
|
makeAdapter({ threadId: "thread-alpha" }),
|
|
164
164
|
makeFactory(),
|
|
@@ -190,7 +190,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
|
|
|
190
190
|
);
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
-
it("reports isMain=true for adapter.threadId on construction
|
|
193
|
+
it("reports isMain=true for adapter.threadId on construction", () => {
|
|
194
194
|
const impl = buildImpl({
|
|
195
195
|
threadId: "thread-alpha",
|
|
196
196
|
threads: [
|
|
@@ -215,7 +215,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
|
|
|
215
215
|
expect(state.threadIds).toEqual(["thread-alpha", "thread-beta"]);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it("does not throw when adapter.threadId has no matching threads entry
|
|
218
|
+
it("does not throw when adapter.threadId has no matching threads entry", () => {
|
|
219
219
|
const impl = buildImpl({ threadId: "thread-alpha" });
|
|
220
220
|
expect(impl.mainItem.getState().id).toBe("thread-alpha");
|
|
221
221
|
expect(impl.mainItem.getState().isMain).toBe(true);
|
|
@@ -234,7 +234,7 @@ describe("ExternalStoreThreadListRuntimeCore - switchToThread", () => {
|
|
|
234
234
|
expect(onSwitchToThread).toHaveBeenCalledWith("thread-beta");
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
it("early-returns without calling onSwitchToThread when target equals initial threadId
|
|
237
|
+
it("early-returns without calling onSwitchToThread when target equals initial threadId", async () => {
|
|
238
238
|
const onSwitchToThread = vi.fn(async () => {});
|
|
239
239
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
240
240
|
makeAdapter({ threadId: "thread-alpha", onSwitchToThread }),
|