@assistant-ui/core 0.3.17 → 0.3.18
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/attachment.d.ts.map +1 -1
- package/dist/adapters/attachment.js +5 -4
- package/dist/adapters/attachment.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.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 +10 -4
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +2 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +30 -7
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +16 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/registry.js +3 -5
- package/dist/model-context/registry.js.map +1 -1
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +5 -4
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/react/client/InMemoryThreadList.js +62 -56
- package/dist/react/client/InMemoryThreadList.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +24 -27
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -0
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +151 -141
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +18 -16
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/interactables-legacy/Interactables.js +242 -121
- package/dist/react/interactables-legacy/Interactables.js.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
- package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
- package/dist/react/model-context/makeAssistantTool.js +1 -1
- package/dist/react/model-context/makeAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantTool.d.ts +2 -2
- package/dist/react/model-context/useAssistantTool.js +1 -1
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +2 -1
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +13 -11
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
- package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
- 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 +3 -0
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +18 -14
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +4 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +1 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +16 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +6 -5
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +1 -1
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +1 -0
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +15 -6
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +1 -1
- package/dist/runtime/utils/thread-message-like.js.map +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 +21 -27
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +2 -2
- package/dist/runtimes/internal.js +2 -2
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
- package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
- package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
- package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
- package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/title.js +9 -1
- package/dist/runtimes/remote-thread-list/title.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +19 -3
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/model-context-client.d.ts.map +1 -1
- package/dist/store/clients/model-context-client.js +2 -1
- package/dist/store/clients/model-context-client.js.map +1 -1
- package/dist/store/clients/single-thread-list.d.ts.map +1 -1
- package/dist/store/clients/single-thread-list.js +1 -0
- package/dist/store/clients/single-thread-list.js.map +1 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +5 -4
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +221 -144
- package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
- 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 +193 -121
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +10 -2
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/message.d.ts +30 -1
- package/dist/store/scopes/message.d.ts.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +2 -2
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +8 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/attachment.d.ts +2 -1
- package/dist/types/attachment.d.ts.map +1 -1
- package/dist/types/attachment.js +2 -1
- package/dist/types/attachment.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts +19 -0
- package/dist/utils/abortable-iterable.d.ts.map +1 -0
- package/dist/utils/abortable-iterable.js +72 -0
- package/dist/utils/abortable-iterable.js.map +1 -0
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/composite-context-provider.js +20 -2
- package/dist/utils/composite-context-provider.js.map +1 -1
- package/dist/utils/record.d.ts +11 -0
- package/dist/utils/record.d.ts.map +1 -0
- package/dist/utils/record.js +12 -0
- package/dist/utils/record.js.map +1 -0
- package/package.json +10 -10
- package/src/adapters/attachment.test.ts +50 -1
- package/src/adapters/attachment.ts +7 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +4 -0
- package/src/model-context/frame/host.test.ts +24 -0
- package/src/model-context/frame/host.ts +10 -4
- package/src/model-context/frame/provider.test.ts +71 -0
- package/src/model-context/frame/provider.ts +37 -7
- package/src/model-context/interactable-composer-metadata.test.ts +28 -0
- package/src/model-context/interactable-composer-metadata.ts +9 -3
- package/src/model-context/registry.test.ts +12 -0
- package/src/model-context/registry.ts +3 -2
- package/src/model-context/types.test.ts +25 -0
- package/src/model-context/types.ts +9 -4
- package/src/react/RuntimeAdapter.test.ts +34 -0
- package/src/react/client/InMemoryThreadList.ts +1 -0
- package/src/react/client/Interactables.test.ts +28 -0
- package/src/react/client/Interactables.ts +15 -13
- package/src/react/client/RemoteThreadList.background.test.ts +148 -6
- package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
- package/src/react/client/RemoteThreadList.test.ts +345 -3
- package/src/react/client/RemoteThreadList.ts +166 -110
- package/src/react/client/Tools.ts +22 -11
- package/src/react/interactables-legacy/Interactables.ts +16 -16
- package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
- package/src/react/model-context/makeAssistantTool.ts +2 -2
- package/src/react/model-context/useAssistantTool.ts +2 -2
- package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
- package/src/react/model-context/useToolArgsStatus.ts +2 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
- package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
- package/src/react/runtimes/RemoteThreadResource.ts +19 -12
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
- package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
- package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
- package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
- package/src/runtime/api/message-runtime.test.ts +75 -0
- package/src/runtime/api/thread-list-item-runtime.ts +10 -3
- package/src/runtime/api/thread-list-runtime.ts +21 -11
- package/src/runtime/api/thread-runtime.test.ts +78 -0
- package/src/runtime/api/thread-runtime.ts +8 -1
- package/src/runtime/base/base-composer-runtime-core.ts +36 -8
- package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
- package/src/runtime/base/base-thread-runtime-core.ts +6 -4
- package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
- package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
- package/src/runtime/utils/external-message-conversion.test.ts +68 -0
- package/src/runtime/utils/external-message-conversion.ts +1 -1
- package/src/runtime/utils/message-repository.ts +29 -17
- package/src/runtime/utils/thread-message-like.ts +1 -1
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
- package/src/runtimes/internal.ts +1 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
- package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
- package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
- package/src/runtimes/remote-thread-list/title.test.ts +62 -0
- package/src/runtimes/remote-thread-list/title.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +10 -0
- package/src/store/clients/external-thread.ts +48 -3
- package/src/store/clients/model-context-client.ts +5 -1
- package/src/store/clients/single-thread-list.ts +1 -0
- package/src/store/clients/suggestions.test.ts +40 -0
- package/src/store/clients/suggestions.ts +13 -10
- package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
- package/src/store/runtime-clients/message-runtime-client.ts +54 -6
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
- package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
- package/src/store/scopes/composer.ts +16 -4
- package/src/store/scopes/message.ts +14 -0
- package/src/store/scopes/thread-list-item.ts +5 -2
- package/src/store/scopes/thread.ts +4 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/MessageRepository.test.ts +116 -0
- package/src/tests/OptimisticState-list-race.test.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
- package/src/tests/attachment-adapters.test.ts +14 -0
- package/src/tests/base-composer-runtime-core.test.ts +39 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
- package/src/tests/external-thread-attachments.test.tsx +139 -0
- package/src/tests/external-thread-parity.test.tsx +23 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
- package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
- package/src/tests/thread-switch-events.test.tsx +78 -0
- package/src/tests/tools-scope-migration.test.tsx +28 -0
- package/src/types/attachment.ts +4 -0
- package/src/utils/abortable-iterable.test.ts +213 -0
- package/src/utils/abortable-iterable.ts +93 -0
- package/src/utils/composite-context-provider.test.ts +25 -0
- package/src/utils/composite-context-provider.ts +23 -2
- package/src/utils/record.test.ts +48 -0
- package/src/utils/record.ts +11 -0
|
@@ -88,15 +88,16 @@ const makeAdapter = (
|
|
|
88
88
|
...overrides,
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const mountThreadList = (
|
|
92
92
|
adapter: RemoteThreadListAdapter,
|
|
93
93
|
tracker: Tracker,
|
|
94
|
+
backgroundThreads = true,
|
|
94
95
|
) => {
|
|
95
96
|
const handle = createAssistantClient(
|
|
96
97
|
AuiConfig({
|
|
97
98
|
threads: RemoteThreadList({
|
|
98
99
|
adapter,
|
|
99
|
-
backgroundThreads
|
|
100
|
+
backgroundThreads,
|
|
100
101
|
thread: (id) =>
|
|
101
102
|
withKey(id, TrackedThread({ threadId: id, tracker }) as never),
|
|
102
103
|
}),
|
|
@@ -112,7 +113,148 @@ const createTracker = (): Tracker => ({
|
|
|
112
113
|
running: new Set(),
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
const deferred = <T>() => {
|
|
117
|
+
let resolve!: (value: T) => void;
|
|
118
|
+
const promise = new Promise<T>((resolvePromise) => {
|
|
119
|
+
resolve = resolvePromise;
|
|
120
|
+
});
|
|
121
|
+
return { promise, resolve };
|
|
122
|
+
};
|
|
123
|
+
|
|
115
124
|
describe("RemoteThreadList backgroundThreads", () => {
|
|
125
|
+
it("keeps the running initialized body when its listed duplicate is selected", async () => {
|
|
126
|
+
const list = deferred<{
|
|
127
|
+
threads: [
|
|
128
|
+
{
|
|
129
|
+
status: "regular";
|
|
130
|
+
remoteId: string;
|
|
131
|
+
externalId: string;
|
|
132
|
+
title: string;
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}>();
|
|
136
|
+
const initialize = deferred<{
|
|
137
|
+
remoteId: string;
|
|
138
|
+
externalId: undefined;
|
|
139
|
+
}>();
|
|
140
|
+
const adapter = makeAdapter({
|
|
141
|
+
list: vi.fn(() => list.promise),
|
|
142
|
+
initialize: vi.fn(() => initialize.promise),
|
|
143
|
+
});
|
|
144
|
+
const tracker = createTracker();
|
|
145
|
+
const handle = mountThreadList(adapter, tracker);
|
|
146
|
+
const aui = handle.getClient();
|
|
147
|
+
const loadPromise = aui.threads.getLoadThreadsPromise();
|
|
148
|
+
const localId = aui.threads.getState().mainThreadId;
|
|
149
|
+
tracker.running.add(localId);
|
|
150
|
+
const initializePromise = aui.threads.item("main").initialize();
|
|
151
|
+
|
|
152
|
+
list.resolve({
|
|
153
|
+
threads: [
|
|
154
|
+
{
|
|
155
|
+
status: "regular",
|
|
156
|
+
remoteId: "remote-1",
|
|
157
|
+
externalId: "remote-1",
|
|
158
|
+
title: "Listed thread",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
await loadPromise;
|
|
163
|
+
flushTapSync(() => aui.threads.switchToThread("remote-1"));
|
|
164
|
+
await vi.waitFor(() => {
|
|
165
|
+
expect(aui.threads.getState().mainThreadId).toBe("remote-1");
|
|
166
|
+
expect(tracker.alive.has("remote-1")).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
const localMountCount = tracker.mounts.filter(
|
|
169
|
+
(id) => id === localId,
|
|
170
|
+
).length;
|
|
171
|
+
|
|
172
|
+
initialize.resolve({ remoteId: "remote-1", externalId: undefined });
|
|
173
|
+
await initializePromise;
|
|
174
|
+
await vi.waitFor(() => {
|
|
175
|
+
expect(aui.threads.getState().mainThreadId).toBe(localId);
|
|
176
|
+
expect(aui.threads.getState().threadIds).toEqual([localId]);
|
|
177
|
+
expect(tracker.alive.has(localId)).toBe(true);
|
|
178
|
+
expect(tracker.alive.has("remote-1")).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(aui.threads.item({ id: "remote-1" }).getState().id).toBe(localId);
|
|
182
|
+
expect(aui.threads.item("main").getState().id).toBe(localId);
|
|
183
|
+
expect(aui.threads.item("main").getState().externalId).toBe("remote-1");
|
|
184
|
+
expect(aui.threads.item("main").getState().title).toBe("Listed thread");
|
|
185
|
+
expect(aui.threads.item("main").getState().isRunning).toBe(true);
|
|
186
|
+
expect(tracker.mounts.filter((id) => id === localId)).toHaveLength(
|
|
187
|
+
localMountCount,
|
|
188
|
+
);
|
|
189
|
+
handle.destroy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("keeps the mounted listed body when an unmounted initialization completes", async () => {
|
|
193
|
+
const list = deferred<{
|
|
194
|
+
threads: [
|
|
195
|
+
{
|
|
196
|
+
status: "regular";
|
|
197
|
+
remoteId: string;
|
|
198
|
+
externalId: string;
|
|
199
|
+
title: string;
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}>();
|
|
203
|
+
const initialize = deferred<{
|
|
204
|
+
remoteId: string;
|
|
205
|
+
externalId: undefined;
|
|
206
|
+
}>();
|
|
207
|
+
const adapter = makeAdapter({
|
|
208
|
+
list: vi.fn(() => list.promise),
|
|
209
|
+
initialize: vi.fn(() => initialize.promise),
|
|
210
|
+
});
|
|
211
|
+
const tracker = createTracker();
|
|
212
|
+
const handle = mountThreadList(adapter, tracker, false);
|
|
213
|
+
const aui = handle.getClient();
|
|
214
|
+
const loadPromise = aui.threads.getLoadThreadsPromise();
|
|
215
|
+
const localId = aui.threads.getState().mainThreadId;
|
|
216
|
+
const initializePromise = aui.threads.item("main").initialize();
|
|
217
|
+
|
|
218
|
+
list.resolve({
|
|
219
|
+
threads: [
|
|
220
|
+
{
|
|
221
|
+
status: "regular",
|
|
222
|
+
remoteId: "remote-1",
|
|
223
|
+
externalId: "external-1",
|
|
224
|
+
title: "Listed thread",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
await loadPromise;
|
|
229
|
+
flushTapSync(() => aui.threads.switchToThread("remote-1"));
|
|
230
|
+
await vi.waitFor(() => {
|
|
231
|
+
expect(aui.threads.getState().mainThreadId).toBe("remote-1");
|
|
232
|
+
expect(tracker.alive.has(localId)).toBe(false);
|
|
233
|
+
expect(tracker.alive.has("remote-1")).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
const listedMountCount = tracker.mounts.filter(
|
|
236
|
+
(id) => id === "remote-1",
|
|
237
|
+
).length;
|
|
238
|
+
|
|
239
|
+
initialize.resolve({ remoteId: "remote-1", externalId: undefined });
|
|
240
|
+
await initializePromise;
|
|
241
|
+
await vi.waitFor(() => {
|
|
242
|
+
expect(aui.threads.getState().mainThreadId).toBe("remote-1");
|
|
243
|
+
expect(aui.threads.getState().threadIds).toEqual(["remote-1"]);
|
|
244
|
+
expect(tracker.alive.has("remote-1")).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(aui.threads.item("main").getState()).toMatchObject({
|
|
248
|
+
id: "remote-1",
|
|
249
|
+
externalId: "external-1",
|
|
250
|
+
title: "Listed thread",
|
|
251
|
+
});
|
|
252
|
+
expect(tracker.mounts.filter((id) => id === "remote-1")).toHaveLength(
|
|
253
|
+
listedMountCount,
|
|
254
|
+
);
|
|
255
|
+
handle.destroy();
|
|
256
|
+
});
|
|
257
|
+
|
|
116
258
|
it("keeps a switched-away thread mounted with live isRunning", async () => {
|
|
117
259
|
const adapter = makeAdapter({
|
|
118
260
|
list: vi.fn(async () => ({
|
|
@@ -120,7 +262,7 @@ describe("RemoteThreadList backgroundThreads", () => {
|
|
|
120
262
|
})),
|
|
121
263
|
});
|
|
122
264
|
const tracker = createTracker();
|
|
123
|
-
const handle =
|
|
265
|
+
const handle = mountThreadList(adapter, tracker);
|
|
124
266
|
const aui = handle.getClient();
|
|
125
267
|
await aui.threads.getLoadThreadsPromise();
|
|
126
268
|
await vi.waitFor(() => {
|
|
@@ -164,7 +306,7 @@ describe("RemoteThreadList backgroundThreads", () => {
|
|
|
164
306
|
})),
|
|
165
307
|
});
|
|
166
308
|
const tracker = createTracker();
|
|
167
|
-
const handle =
|
|
309
|
+
const handle = mountThreadList(adapter, tracker);
|
|
168
310
|
const aui = handle.getClient();
|
|
169
311
|
await aui.threads.getLoadThreadsPromise();
|
|
170
312
|
await vi.waitFor(() => {
|
|
@@ -260,7 +402,7 @@ describe("RemoteThreadList backgroundThreads", () => {
|
|
|
260
402
|
});
|
|
261
403
|
const tracker = createTracker();
|
|
262
404
|
tracker.messagesOf = () => [{ status: { type: "complete" } }];
|
|
263
|
-
const handle =
|
|
405
|
+
const handle = mountThreadList(adapter, tracker);
|
|
264
406
|
const aui = handle.getClient();
|
|
265
407
|
await aui.threads.getLoadThreadsPromise();
|
|
266
408
|
await vi.waitFor(() => {
|
|
@@ -350,7 +492,7 @@ describe("RemoteThreadList backgroundThreads", () => {
|
|
|
350
492
|
list: vi.fn(async () => ({ threads: listed })),
|
|
351
493
|
});
|
|
352
494
|
const tracker = createTracker();
|
|
353
|
-
const handle =
|
|
495
|
+
const handle = mountThreadList(adapter, tracker);
|
|
354
496
|
const aui = handle.getClient();
|
|
355
497
|
await aui.threads.getLoadThreadsPromise();
|
|
356
498
|
const localId = aui.threads.getState().mainThreadId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { createRef, startTransition, Suspense } from "react";
|
|
3
|
+
import { createRef, startTransition, Suspense, useLayoutEffect } from "react";
|
|
4
4
|
import { act, render, waitFor } from "@testing-library/react";
|
|
5
5
|
import { resource, withKey } from "@assistant-ui/tap";
|
|
6
6
|
import {
|
|
@@ -68,6 +68,47 @@ const makeAdapter = (): RemoteThreadListAdapter => ({
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
describe("RemoteThreadList concurrent rendering", () => {
|
|
71
|
+
it("reloads through the committed adapter from a descendant layout effect", async () => {
|
|
72
|
+
const adapterA = makeAdapter();
|
|
73
|
+
const adapterB = makeAdapter();
|
|
74
|
+
|
|
75
|
+
const ReloadOnSwap = ({ workspace }: { workspace: string }) => {
|
|
76
|
+
const aui = useAui();
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
if (workspace === "A") return;
|
|
79
|
+
void aui.threads.reload();
|
|
80
|
+
}, [aui, workspace]);
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
const App = ({
|
|
84
|
+
adapter,
|
|
85
|
+
workspace,
|
|
86
|
+
}: {
|
|
87
|
+
adapter: RemoteThreadListAdapter;
|
|
88
|
+
workspace: string;
|
|
89
|
+
}) => (
|
|
90
|
+
<AuiProvider
|
|
91
|
+
config={AuiConfig({
|
|
92
|
+
threads: RemoteThreadList({
|
|
93
|
+
adapter,
|
|
94
|
+
thread: () => StubThread() as never,
|
|
95
|
+
}),
|
|
96
|
+
})}
|
|
97
|
+
>
|
|
98
|
+
<ReloadOnSwap workspace={workspace} />
|
|
99
|
+
</AuiProvider>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const view = render(<App adapter={adapterA} workspace="A" />);
|
|
103
|
+
await waitFor(() => expect(adapterA.list).toHaveBeenCalledOnce());
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
view.rerender(<App adapter={adapterB} workspace="B" />);
|
|
107
|
+
});
|
|
108
|
+
await waitFor(() => expect(adapterB.list).toHaveBeenCalled());
|
|
109
|
+
expect(adapterA.list).toHaveBeenCalledOnce();
|
|
110
|
+
});
|
|
111
|
+
|
|
71
112
|
it("keeps actions scoped to the committed adapter", async () => {
|
|
72
113
|
const adapterA = makeAdapter();
|
|
73
114
|
const adapterB = makeAdapter();
|
|
@@ -117,6 +117,7 @@ const mountList = (
|
|
|
117
117
|
threadId?: string,
|
|
118
118
|
refetch?: () => Promise<void>,
|
|
119
119
|
onSwitchToThread?: (id: string) => void,
|
|
120
|
+
onDelete?: (id: string) => void,
|
|
120
121
|
) => {
|
|
121
122
|
const onThreadIdChange = vi.fn();
|
|
122
123
|
const handle = createAssistantClient(
|
|
@@ -127,6 +128,7 @@ const mountList = (
|
|
|
127
128
|
threadId,
|
|
128
129
|
onThreadIdChange,
|
|
129
130
|
onSwitchToThread,
|
|
131
|
+
onDelete,
|
|
130
132
|
}),
|
|
131
133
|
}),
|
|
132
134
|
);
|
|
@@ -402,6 +404,141 @@ describe("RemoteThreadList", () => {
|
|
|
402
404
|
handle.destroy();
|
|
403
405
|
});
|
|
404
406
|
|
|
407
|
+
it("clears the deleted thread's title state so a reborn slot can auto-title", async () => {
|
|
408
|
+
const adapter = makeAdapter({
|
|
409
|
+
list: vi.fn(async () => ({
|
|
410
|
+
threads: [
|
|
411
|
+
{ status: "regular" as const, remoteId: "t1", title: "One" },
|
|
412
|
+
{ status: "regular" as const, remoteId: "t2", title: "Two" },
|
|
413
|
+
],
|
|
414
|
+
})),
|
|
415
|
+
});
|
|
416
|
+
const { handle } = mountList(adapter);
|
|
417
|
+
const aui = handle.getClient();
|
|
418
|
+
await aui.threads.getLoadThreadsPromise();
|
|
419
|
+
await vi.waitFor(() => {
|
|
420
|
+
expect(aui.threads.getState().threadIds).toEqual(["t1", "t2"]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await aui.threads.item({ id: "t1" }).rename("Manual title");
|
|
424
|
+
await aui.threads.item({ id: "t2" }).rename("Kept manual title");
|
|
425
|
+
await aui.threads.item({ id: "t1" }).delete();
|
|
426
|
+
await vi.waitFor(() => {
|
|
427
|
+
expect(aui.threads.getState().threadIds).toEqual(["t2"]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// The server still lists t1, so a reload rebirths the slot under the same
|
|
431
|
+
// remote id; a leaked manual-rename entry would suppress its auto-title.
|
|
432
|
+
await aui.threads.reload();
|
|
433
|
+
await vi.waitFor(() => {
|
|
434
|
+
expect(aui.threads.getState().threadIds).toEqual(["t1", "t2"]);
|
|
435
|
+
});
|
|
436
|
+
flushTapSync(() => aui.threads.switchToThread("t1"));
|
|
437
|
+
await vi.waitFor(() => {
|
|
438
|
+
expect(aui.threads.getState().mainThreadId).toBe("t1");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await aui.threads.item({ id: "t1" }).generateTitle({ automatic: true });
|
|
442
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
443
|
+
|
|
444
|
+
// t2 is the control: a reload that cleared every title state would
|
|
445
|
+
// generate here as well.
|
|
446
|
+
flushTapSync(() => aui.threads.switchToThread("t2"));
|
|
447
|
+
await vi.waitFor(() => {
|
|
448
|
+
expect(aui.threads.getState().mainThreadId).toBe("t2");
|
|
449
|
+
});
|
|
450
|
+
await aui.threads.item({ id: "t2" }).generateTitle({ automatic: true });
|
|
451
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
452
|
+
handle.destroy();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("preserves an existing title when generation returns no title", async () => {
|
|
456
|
+
const adapter = makeAdapter({
|
|
457
|
+
list: vi.fn(async () => ({
|
|
458
|
+
threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
|
|
459
|
+
})),
|
|
460
|
+
});
|
|
461
|
+
const { handle } = mountList(adapter);
|
|
462
|
+
const aui = handle.getClient();
|
|
463
|
+
await aui.threads.getLoadThreadsPromise();
|
|
464
|
+
await vi.waitFor(() => {
|
|
465
|
+
expect(aui.threads.getState().threadIds).toEqual(["t1"]);
|
|
466
|
+
});
|
|
467
|
+
flushTapSync(() => aui.threads.switchToThread("t1"));
|
|
468
|
+
await vi.waitFor(() => {
|
|
469
|
+
expect(aui.threads.getState().mainThreadId).toBe("t1");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await aui.threads.item({ id: "t1" }).generateTitle();
|
|
473
|
+
// The list item re-renders a tick after the generation resolves, so an
|
|
474
|
+
// erasing title write lands only once that render has run.
|
|
475
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
476
|
+
|
|
477
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
478
|
+
expect(aui.threads.item({ id: "t1" }).getState().title).toBe("One");
|
|
479
|
+
handle.destroy();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("keeps a manual rename made during automatic title generation", async () => {
|
|
483
|
+
const generatedTitle = deferred<ReadableStream>();
|
|
484
|
+
const adapter = makeAdapter({
|
|
485
|
+
list: vi.fn(async () => ({
|
|
486
|
+
threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
|
|
487
|
+
})),
|
|
488
|
+
generateTitle: vi.fn(async () => generatedTitle.promise as never),
|
|
489
|
+
});
|
|
490
|
+
const { handle } = mountList(adapter);
|
|
491
|
+
const aui = handle.getClient();
|
|
492
|
+
await aui.threads.getLoadThreadsPromise();
|
|
493
|
+
await vi.waitFor(() => {
|
|
494
|
+
expect(aui.threads.getState().threadIds).toEqual(["t1"]);
|
|
495
|
+
});
|
|
496
|
+
flushTapSync(() => aui.threads.switchToThread("t1"));
|
|
497
|
+
await vi.waitFor(() => {
|
|
498
|
+
expect(aui.threads.getState().mainThreadId).toBe("t1");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const generation = aui.threads
|
|
502
|
+
.item({ id: "t1" })
|
|
503
|
+
.generateTitle({ automatic: true });
|
|
504
|
+
await vi.waitFor(() => {
|
|
505
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
506
|
+
});
|
|
507
|
+
await aui.threads.item({ id: "t1" }).rename("Manual title");
|
|
508
|
+
await vi.waitFor(() => {
|
|
509
|
+
expect(aui.threads.item({ id: "t1" }).getState().title).toBe(
|
|
510
|
+
"Manual title",
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
generatedTitle.resolve(
|
|
515
|
+
new ReadableStream({
|
|
516
|
+
start(controller) {
|
|
517
|
+
controller.enqueue({
|
|
518
|
+
type: "part-start",
|
|
519
|
+
path: [0],
|
|
520
|
+
part: { type: "text" },
|
|
521
|
+
});
|
|
522
|
+
controller.enqueue({
|
|
523
|
+
type: "text-delta",
|
|
524
|
+
path: [0],
|
|
525
|
+
textDelta: "Generated title",
|
|
526
|
+
});
|
|
527
|
+
controller.enqueue({ type: "part-finish", path: [0] });
|
|
528
|
+
controller.close();
|
|
529
|
+
},
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
await generation;
|
|
533
|
+
|
|
534
|
+
expect(aui.threads.item({ id: "t1" }).getState().title).toBe(
|
|
535
|
+
"Manual title",
|
|
536
|
+
);
|
|
537
|
+
expect(adapter.rename).toHaveBeenNthCalledWith(1, "t1", "Manual title");
|
|
538
|
+
expect(adapter.rename).toHaveBeenNthCalledWith(2, "t1", "Manual title");
|
|
539
|
+
handle.destroy();
|
|
540
|
+
});
|
|
541
|
+
|
|
405
542
|
it("preserves generated titles across an overlapping reload", async () => {
|
|
406
543
|
const reload = deferred<{
|
|
407
544
|
threads: {
|
|
@@ -623,10 +760,17 @@ describe("RemoteThreadList", () => {
|
|
|
623
760
|
});
|
|
624
761
|
|
|
625
762
|
it("keeps one slot per remote id across reload and clears it on delete", async () => {
|
|
763
|
+
const onDelete = vi.fn();
|
|
626
764
|
const adapter = makeAdapter({
|
|
627
765
|
list: vi.fn(async () => ({ threads: [] })),
|
|
628
766
|
});
|
|
629
|
-
const { handle } = mountList(
|
|
767
|
+
const { handle } = mountList(
|
|
768
|
+
adapter,
|
|
769
|
+
undefined,
|
|
770
|
+
undefined,
|
|
771
|
+
undefined,
|
|
772
|
+
onDelete,
|
|
773
|
+
);
|
|
630
774
|
const aui = handle.getClient();
|
|
631
775
|
await aui.threads.getLoadThreadsPromise();
|
|
632
776
|
const localId = aui.threads.getState().mainThreadId;
|
|
@@ -646,6 +790,120 @@ describe("RemoteThreadList", () => {
|
|
|
646
790
|
});
|
|
647
791
|
expect(() => aui.threads.item({ id: localId }).getState()).toThrow();
|
|
648
792
|
expect(() => aui.threads.item({ id: remoteId }).getState()).toThrow();
|
|
793
|
+
expect(onDelete).toHaveBeenCalledOnce();
|
|
794
|
+
expect(onDelete).toHaveBeenCalledWith(localId);
|
|
795
|
+
handle.destroy();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("does not invoke deletion cleanup when the remote deletion fails", async () => {
|
|
799
|
+
const error = new Error("delete failed");
|
|
800
|
+
const onDelete = vi.fn();
|
|
801
|
+
const adapter = makeAdapter({
|
|
802
|
+
list: vi.fn(async () => ({
|
|
803
|
+
threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
|
|
804
|
+
})),
|
|
805
|
+
delete: vi.fn(async () => {
|
|
806
|
+
throw error;
|
|
807
|
+
}),
|
|
808
|
+
});
|
|
809
|
+
const { handle } = mountList(
|
|
810
|
+
adapter,
|
|
811
|
+
undefined,
|
|
812
|
+
undefined,
|
|
813
|
+
undefined,
|
|
814
|
+
onDelete,
|
|
815
|
+
);
|
|
816
|
+
const aui = handle.getClient();
|
|
817
|
+
await aui.threads.getLoadThreadsPromise();
|
|
818
|
+
await vi.waitFor(() => {
|
|
819
|
+
expect(aui.threads.getState().threadIds).toContain("t1");
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
await expect(aui.threads.item({ id: "t1" }).delete()).rejects.toBe(error);
|
|
823
|
+
|
|
824
|
+
expect(aui.threads.getState().threadIds).toContain("t1");
|
|
825
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
826
|
+
handle.destroy();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const deleteDuringAdapterSwap = async (
|
|
830
|
+
replacementThreads: readonly {
|
|
831
|
+
status: "regular";
|
|
832
|
+
remoteId: string;
|
|
833
|
+
title: string;
|
|
834
|
+
}[],
|
|
835
|
+
) => {
|
|
836
|
+
const removal = deferred<void>();
|
|
837
|
+
const onDelete = vi.fn();
|
|
838
|
+
const adapterA = makeAdapter({
|
|
839
|
+
list: vi.fn(async () => ({
|
|
840
|
+
threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
|
|
841
|
+
})),
|
|
842
|
+
delete: vi.fn(() => removal.promise),
|
|
843
|
+
});
|
|
844
|
+
const adapterB = makeAdapter({
|
|
845
|
+
list: vi.fn(async () => ({ threads: replacementThreads })),
|
|
846
|
+
});
|
|
847
|
+
let adapter: RemoteThreadListAdapter = adapterA;
|
|
848
|
+
const listeners = new Set<() => void>();
|
|
849
|
+
const source: AssistantConfigSource = {
|
|
850
|
+
getConfig: () =>
|
|
851
|
+
AuiConfig({
|
|
852
|
+
threads: RemoteThreadList({
|
|
853
|
+
adapter,
|
|
854
|
+
thread: (id) => StubThread({ threadId: id }) as never,
|
|
855
|
+
onDelete,
|
|
856
|
+
}),
|
|
857
|
+
}),
|
|
858
|
+
subscribe: (listener) => {
|
|
859
|
+
listeners.add(listener);
|
|
860
|
+
return () => {
|
|
861
|
+
listeners.delete(listener);
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
const handle = createAssistantClient(source);
|
|
866
|
+
handle.subscribe(() => {});
|
|
867
|
+
await handle.getClient().threads.getLoadThreadsPromise();
|
|
868
|
+
await vi.waitFor(() => {
|
|
869
|
+
expect(handle.getClient().threads.getState().threadIds).toContain("t1");
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const deletion = handle.getClient().threads.item({ id: "t1" }).delete();
|
|
873
|
+
await vi.waitFor(() => {
|
|
874
|
+
expect(adapterA.delete).toHaveBeenCalledWith("t1");
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
adapter = adapterB;
|
|
878
|
+
for (const listener of listeners) listener();
|
|
879
|
+
await vi.waitFor(async () => {
|
|
880
|
+
flushTapSync(() => {});
|
|
881
|
+
await handle.getClient().threads.reload();
|
|
882
|
+
expect(adapterB.list).toHaveBeenCalled();
|
|
883
|
+
expect(handle.getClient().threads.getState().threadIds).toEqual(
|
|
884
|
+
replacementThreads.map((thread) => thread.remoteId),
|
|
885
|
+
);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
removal.resolve();
|
|
889
|
+
await deletion;
|
|
890
|
+
return { handle, onDelete };
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
it("invokes deletion cleanup when the adapter changes during a successful deletion", async () => {
|
|
894
|
+
const { handle, onDelete } = await deleteDuringAdapterSwap([]);
|
|
895
|
+
|
|
896
|
+
expect(onDelete).toHaveBeenCalledWith("t1");
|
|
897
|
+
handle.destroy();
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it("skips deletion cleanup when the replacement adapter re-lists the deleted id", async () => {
|
|
901
|
+
const { handle, onDelete } = await deleteDuringAdapterSwap([
|
|
902
|
+
{ status: "regular" as const, remoteId: "t1", title: "One" },
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
expect(handle.getClient().threads.getState().threadIds).toContain("t1");
|
|
906
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
649
907
|
handle.destroy();
|
|
650
908
|
});
|
|
651
909
|
|
|
@@ -873,6 +1131,7 @@ describe("RemoteThreadList", () => {
|
|
|
873
1131
|
});
|
|
874
1132
|
|
|
875
1133
|
it("does not reset again when retrying a failed replacement load", async () => {
|
|
1134
|
+
const error = new Error("network");
|
|
876
1135
|
const methodsA = makeAdapter({
|
|
877
1136
|
list: vi.fn(async () => ({
|
|
878
1137
|
threads: [
|
|
@@ -883,8 +1142,8 @@ describe("RemoteThreadList", () => {
|
|
|
883
1142
|
const methodsB = makeAdapter({
|
|
884
1143
|
list: vi
|
|
885
1144
|
.fn()
|
|
886
|
-
.mockRejectedValueOnce(
|
|
887
|
-
.
|
|
1145
|
+
.mockRejectedValueOnce(error)
|
|
1146
|
+
.mockResolvedValue({
|
|
888
1147
|
threads: [
|
|
889
1148
|
{ status: "regular" as const, remoteId: "thread-b", title: "B" },
|
|
890
1149
|
],
|
|
@@ -933,6 +1192,7 @@ describe("RemoteThreadList", () => {
|
|
|
933
1192
|
const mainAfterFailure = handle
|
|
934
1193
|
.getClient()
|
|
935
1194
|
.threads.getState().mainThreadId;
|
|
1195
|
+
expect(handle.getClient().threads.getState().loadError).toBe(error);
|
|
936
1196
|
await handle.getClient().threads.reload();
|
|
937
1197
|
await vi.waitFor(() => {
|
|
938
1198
|
expect(handle.getClient().threads.getState().threadIds).toEqual([
|
|
@@ -942,12 +1202,94 @@ describe("RemoteThreadList", () => {
|
|
|
942
1202
|
expect(handle.getClient().threads.getState().mainThreadId).toBe(
|
|
943
1203
|
mainAfterFailure,
|
|
944
1204
|
);
|
|
1205
|
+
expect(handle.getClient().threads.getState().loadError).toBeUndefined();
|
|
945
1206
|
} finally {
|
|
946
1207
|
consoleError.mockRestore();
|
|
947
1208
|
}
|
|
948
1209
|
handle.destroy();
|
|
949
1210
|
});
|
|
950
1211
|
|
|
1212
|
+
it("reloads once when the browser comes online after a failed load", async () => {
|
|
1213
|
+
vi.stubGlobal("window", new EventTarget());
|
|
1214
|
+
vi.stubGlobal(
|
|
1215
|
+
"document",
|
|
1216
|
+
Object.assign(new EventTarget(), { visibilityState: "visible" }),
|
|
1217
|
+
);
|
|
1218
|
+
const error = new Error("offline");
|
|
1219
|
+
const firstLoad = deferred<{ threads: [] }>();
|
|
1220
|
+
const list = vi
|
|
1221
|
+
.fn()
|
|
1222
|
+
.mockReturnValueOnce(firstLoad.promise)
|
|
1223
|
+
.mockResolvedValueOnce({ threads: [] });
|
|
1224
|
+
const consoleError = vi
|
|
1225
|
+
.spyOn(console, "error")
|
|
1226
|
+
.mockImplementation(() => {});
|
|
1227
|
+
const { handle } = mountList(makeAdapter({ list }));
|
|
1228
|
+
|
|
1229
|
+
try {
|
|
1230
|
+
await vi.waitFor(() => expect(list).toHaveBeenCalledTimes(1));
|
|
1231
|
+
firstLoad.reject(error);
|
|
1232
|
+
await vi.waitFor(() =>
|
|
1233
|
+
expect(handle.getClient().threads.getState().loadError).toBe(error),
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
window.dispatchEvent(new Event("online"));
|
|
1237
|
+
|
|
1238
|
+
await vi.waitFor(() => expect(list).toHaveBeenCalledTimes(2));
|
|
1239
|
+
await vi.waitFor(() =>
|
|
1240
|
+
expect(handle.getClient().threads.getState().loadError).toBeUndefined(),
|
|
1241
|
+
);
|
|
1242
|
+
expect(list).toHaveBeenCalledTimes(2);
|
|
1243
|
+
} finally {
|
|
1244
|
+
handle.destroy();
|
|
1245
|
+
consoleError.mockRestore();
|
|
1246
|
+
vi.unstubAllGlobals();
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it("retries once when the document becomes visible after a failed load", async () => {
|
|
1251
|
+
vi.stubGlobal("window", new EventTarget());
|
|
1252
|
+
const document = Object.assign(new EventTarget(), {
|
|
1253
|
+
visibilityState: "hidden",
|
|
1254
|
+
});
|
|
1255
|
+
vi.stubGlobal("document", document);
|
|
1256
|
+
const error = new Error("offline");
|
|
1257
|
+
const firstLoad = deferred<{ threads: [] }>();
|
|
1258
|
+
const list = vi
|
|
1259
|
+
.fn()
|
|
1260
|
+
.mockReturnValueOnce(firstLoad.promise)
|
|
1261
|
+
.mockResolvedValueOnce({ threads: [] });
|
|
1262
|
+
const consoleError = vi
|
|
1263
|
+
.spyOn(console, "error")
|
|
1264
|
+
.mockImplementation(() => {});
|
|
1265
|
+
const { handle } = mountList(makeAdapter({ list }));
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
await vi.waitFor(() => expect(list).toHaveBeenCalledTimes(1));
|
|
1269
|
+
firstLoad.reject(error);
|
|
1270
|
+
await vi.waitFor(() =>
|
|
1271
|
+
expect(handle.getClient().threads.getState().loadError).toBe(error),
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
1275
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1276
|
+
expect(list).toHaveBeenCalledTimes(1);
|
|
1277
|
+
|
|
1278
|
+
document.visibilityState = "visible";
|
|
1279
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
1280
|
+
|
|
1281
|
+
await vi.waitFor(() => expect(list).toHaveBeenCalledTimes(2));
|
|
1282
|
+
await vi.waitFor(() =>
|
|
1283
|
+
expect(handle.getClient().threads.getState().loadError).toBeUndefined(),
|
|
1284
|
+
);
|
|
1285
|
+
expect(list).toHaveBeenCalledTimes(2);
|
|
1286
|
+
} finally {
|
|
1287
|
+
handle.destroy();
|
|
1288
|
+
consoleError.mockRestore();
|
|
1289
|
+
vi.unstubAllGlobals();
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
951
1293
|
it("does not send a superseded rename through the replacement adapter", async () => {
|
|
952
1294
|
const initializeRequest = deferred<{
|
|
953
1295
|
remoteId: string;
|