@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
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
* Props used to register a tool from React.
|
|
13
13
|
*
|
|
14
14
|
* @deprecated Use a toolkit with `Tools({ toolkit })` and register it via
|
|
15
|
-
* `
|
|
15
|
+
* `AuiConfig({ tools: Tools({ toolkit }) })` on the provider's `config` prop instead. See
|
|
16
16
|
* https://assistant-ui.com/docs/migrations/toolkit-tools.
|
|
17
17
|
*/
|
|
18
18
|
export type AssistantToolProps<
|
|
@@ -39,7 +39,7 @@ export type AssistantToolProps<
|
|
|
39
39
|
* @param tool - Tool definition and name to register.
|
|
40
40
|
*
|
|
41
41
|
* @deprecated Use a toolkit with `Tools({ toolkit })` and register it via
|
|
42
|
-
* `
|
|
42
|
+
* `AuiConfig({ tools: Tools({ toolkit }) })` on the provider's `config` prop instead. See
|
|
43
43
|
* https://assistant-ui.com/docs/migrations/toolkit-tools.
|
|
44
44
|
*
|
|
45
45
|
* @example
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const part = vi.hoisted(() => ({
|
|
7
|
+
type: "tool-call" as const,
|
|
8
|
+
status: { type: "complete" as const },
|
|
9
|
+
args: JSON.parse('{"__proto__":"value"}') as Record<string, unknown>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("@assistant-ui/store")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
useAuiState: (selector: (state: { part: typeof part }) => unknown) =>
|
|
17
|
+
selector({ part }),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import { useToolArgsStatus } from "./useToolArgsStatus";
|
|
22
|
+
|
|
23
|
+
describe("useToolArgsStatus", () => {
|
|
24
|
+
it("reports status for an argument named __proto__", () => {
|
|
25
|
+
const { result } = renderHook(() => useToolArgsStatus());
|
|
26
|
+
|
|
27
|
+
expect(Object.hasOwn(result.current.propStatus, "__proto__")).toBe(true);
|
|
28
|
+
expect(result.current.propStatus.__proto__).toBe("complete");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getPartialJsonObjectFieldState,
|
|
5
5
|
getPartialJsonObjectMeta,
|
|
6
6
|
} from "assistant-stream/utils";
|
|
7
|
+
import { nullProtoRecord } from "../../utils/record";
|
|
7
8
|
|
|
8
9
|
type PropFieldStatus = "streaming" | "complete";
|
|
9
10
|
|
|
@@ -60,7 +61,7 @@ export const useToolArgsStatus = <
|
|
|
60
61
|
const isStreaming = statusType === "running";
|
|
61
62
|
const args = part.args as Record<string, unknown>;
|
|
62
63
|
const meta = getPartialJsonObjectMeta(args as Record<symbol, unknown>);
|
|
63
|
-
const propStatus
|
|
64
|
+
const propStatus = nullProtoRecord<PropFieldStatus>();
|
|
64
65
|
|
|
65
66
|
for (const key of Object.keys(args)) {
|
|
66
67
|
if (meta) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
|
|
3
|
+
import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
|
|
4
|
+
import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
|
|
5
|
+
|
|
6
|
+
const makeManager = () =>
|
|
7
|
+
new RemoteThreadListHookInstanceManager(
|
|
8
|
+
() => ({}) as never,
|
|
9
|
+
{} as ThreadListRuntimeCore,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// No AdapterSink attaches a runtime here, so the start and restart promises
|
|
13
|
+
// stay pending or reject on stop; neither is what these tests are about.
|
|
14
|
+
const start = (manager: RemoteThreadListHookInstanceManager, id: string) => {
|
|
15
|
+
manager.startThreadRuntime(id).catch(() => {});
|
|
16
|
+
};
|
|
17
|
+
const restart = (manager: RemoteThreadListHookInstanceManager, id: string) => {
|
|
18
|
+
manager.__internal_restartThreadRuntime(id).catch(() => {});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const publishedSignal = (
|
|
22
|
+
manager: RemoteThreadListHookInstanceManager,
|
|
23
|
+
id: string,
|
|
24
|
+
) => {
|
|
25
|
+
const { hostStore } = manager as unknown as {
|
|
26
|
+
hostStore: {
|
|
27
|
+
getState: () => {
|
|
28
|
+
threads: readonly { id: string; destroySignal: AbortSignal }[];
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
return hostStore.getState().threads.find((thread) => thread.id === id)
|
|
33
|
+
?.destroySignal;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("RemoteThreadListHookInstanceManager destroy signal", () => {
|
|
37
|
+
it("publishes a live signal for each started thread", () => {
|
|
38
|
+
const manager = makeManager();
|
|
39
|
+
start(manager, "thread-1");
|
|
40
|
+
|
|
41
|
+
expect(publishedSignal(manager, "thread-1")?.aborted).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("aborts the thread's signal when its runtime stops", () => {
|
|
45
|
+
const manager = makeManager();
|
|
46
|
+
start(manager, "thread-1");
|
|
47
|
+
const signal = publishedSignal(manager, "thread-1")!;
|
|
48
|
+
|
|
49
|
+
manager.stopThreadRuntime("thread-1");
|
|
50
|
+
|
|
51
|
+
expect(signal.aborted).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves sibling threads untouched when one stops", () => {
|
|
55
|
+
const manager = makeManager();
|
|
56
|
+
start(manager, "thread-1");
|
|
57
|
+
start(manager, "thread-2");
|
|
58
|
+
const sibling = publishedSignal(manager, "thread-2")!;
|
|
59
|
+
|
|
60
|
+
manager.stopThreadRuntime("thread-1");
|
|
61
|
+
|
|
62
|
+
expect(sibling.aborted).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("hands the next generation a fresh signal on restart", () => {
|
|
66
|
+
const manager = makeManager();
|
|
67
|
+
start(manager, "thread-1");
|
|
68
|
+
const first = publishedSignal(manager, "thread-1")!;
|
|
69
|
+
|
|
70
|
+
restart(manager, "thread-1");
|
|
71
|
+
const second = publishedSignal(manager, "thread-1")!;
|
|
72
|
+
|
|
73
|
+
expect(first.aborted).toBe(true);
|
|
74
|
+
expect(second).not.toBe(first);
|
|
75
|
+
expect(second.aborted).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("aborts every thread when the manager is disposed", () => {
|
|
79
|
+
const manager = makeManager();
|
|
80
|
+
start(manager, "thread-1");
|
|
81
|
+
start(manager, "thread-2");
|
|
82
|
+
const signals = [
|
|
83
|
+
publishedSignal(manager, "thread-1")!,
|
|
84
|
+
publishedSignal(manager, "thread-2")!,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
manager.__internal_dispose();
|
|
88
|
+
|
|
89
|
+
expect(signals.map((signal) => signal.aborted)).toEqual([true, true]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const makeRunningRuntime = () => {
|
|
94
|
+
const subscribers = new Set<() => void>();
|
|
95
|
+
const eventListeners = new Map<string, Set<() => void>>();
|
|
96
|
+
const runtime = {
|
|
97
|
+
isRunning: true,
|
|
98
|
+
messages: [],
|
|
99
|
+
subscribe: (callback: () => void) => {
|
|
100
|
+
subscribers.add(callback);
|
|
101
|
+
return () => subscribers.delete(callback);
|
|
102
|
+
},
|
|
103
|
+
unstable_on: (event: string, callback: () => void) => {
|
|
104
|
+
let listeners = eventListeners.get(event);
|
|
105
|
+
if (!listeners) {
|
|
106
|
+
listeners = new Set();
|
|
107
|
+
eventListeners.set(event, listeners);
|
|
108
|
+
}
|
|
109
|
+
listeners.add(callback);
|
|
110
|
+
return () => listeners.delete(callback);
|
|
111
|
+
},
|
|
112
|
+
} as unknown as ThreadRuntimeCore & { isRunning: boolean };
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
runtime,
|
|
116
|
+
// what a stopOnClientDestroy consumer does when the destroy signal fires
|
|
117
|
+
stop: () => {
|
|
118
|
+
runtime.isRunning = false;
|
|
119
|
+
for (const callback of eventListeners.get("runEnd") ?? []) callback();
|
|
120
|
+
for (const callback of subscribers) callback();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const publish = (
|
|
126
|
+
manager: RemoteThreadListHookInstanceManager,
|
|
127
|
+
threadId: string,
|
|
128
|
+
runtime: ThreadRuntimeCore,
|
|
129
|
+
) => {
|
|
130
|
+
const internals = manager as unknown as {
|
|
131
|
+
instances: Map<string, { generation: number }>;
|
|
132
|
+
_publishThreadRuntime: (
|
|
133
|
+
threadId: string,
|
|
134
|
+
runtime: ThreadRuntimeCore,
|
|
135
|
+
generation: number,
|
|
136
|
+
) => void;
|
|
137
|
+
};
|
|
138
|
+
internals._publishThreadRuntime(
|
|
139
|
+
threadId,
|
|
140
|
+
runtime,
|
|
141
|
+
internals.instances.get(threadId)!.generation,
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
describe("RemoteThreadListHookInstanceManager restart teardown", () => {
|
|
146
|
+
it("keeps the outgoing generation's terminal events off the thread's subscribers", () => {
|
|
147
|
+
const manager = makeManager();
|
|
148
|
+
start(manager, "thread-1");
|
|
149
|
+
const { runtime, stop } = makeRunningRuntime();
|
|
150
|
+
publish(manager, "thread-1", runtime);
|
|
151
|
+
|
|
152
|
+
const signal = publishedSignal(manager, "thread-1")!;
|
|
153
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
154
|
+
|
|
155
|
+
const events: string[] = [];
|
|
156
|
+
manager.__internal_subscribeThreadEvents((event) =>
|
|
157
|
+
events.push(event.type),
|
|
158
|
+
);
|
|
159
|
+
const runningChanges: boolean[] = [];
|
|
160
|
+
manager.__internal_subscribeRunningChanged(() =>
|
|
161
|
+
runningChanges.push(manager.__internal_isThreadRunning("thread-1")),
|
|
162
|
+
);
|
|
163
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
|
|
164
|
+
|
|
165
|
+
restart(manager, "thread-1");
|
|
166
|
+
|
|
167
|
+
expect(signal.aborted).toBe(true);
|
|
168
|
+
expect(events).toEqual([]);
|
|
169
|
+
expect(runningChanges).toEqual([]);
|
|
170
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -155,6 +155,39 @@ describe("RemoteThreadListHookInstanceManager run tracking", () => {
|
|
|
155
155
|
expect(onChange).toHaveBeenCalledTimes(2);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
it("notifies every running subscriber when one throws", () => {
|
|
159
|
+
const manager = makeManager();
|
|
160
|
+
start(manager, "thread-1");
|
|
161
|
+
const thread = makeRuntime({ isRunning: false });
|
|
162
|
+
publish(manager, "thread-1", thread.runtime);
|
|
163
|
+
const error = new Error("listener failed");
|
|
164
|
+
manager.__internal_subscribeRunningChanged(() => {
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
const laterSubscriber = vi.fn();
|
|
168
|
+
manager.__internal_subscribeRunningChanged(laterSubscriber);
|
|
169
|
+
|
|
170
|
+
expect(() => thread.setRunning(true)).toThrow(error);
|
|
171
|
+
expect(laterSubscriber).toHaveBeenCalledOnce();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("notifies every replacement subscriber when one throws", () => {
|
|
175
|
+
const manager = makeManager();
|
|
176
|
+
start(manager, "thread-1");
|
|
177
|
+
publish(manager, "thread-1", makeRuntime().runtime);
|
|
178
|
+
const error = new Error("listener failed");
|
|
179
|
+
manager.__internal_subscribeRuntimeReplaced(() => {
|
|
180
|
+
throw error;
|
|
181
|
+
});
|
|
182
|
+
const laterSubscriber = vi.fn();
|
|
183
|
+
manager.__internal_subscribeRuntimeReplaced(laterSubscriber);
|
|
184
|
+
|
|
185
|
+
expect(() => publish(manager, "thread-1", makeRuntime().runtime)).toThrow(
|
|
186
|
+
error,
|
|
187
|
+
);
|
|
188
|
+
expect(laterSubscriber).toHaveBeenCalledOnce();
|
|
189
|
+
});
|
|
190
|
+
|
|
158
191
|
it("moves tracking to the runtime a restart publishes", () => {
|
|
159
192
|
const manager = makeManager();
|
|
160
193
|
start(manager, "thread-1");
|
|
@@ -37,13 +37,19 @@ describe("RemoteThreadListHookInstanceManager", () => {
|
|
|
37
37
|
const internals = manager as unknown as {
|
|
38
38
|
instances: Map<
|
|
39
39
|
string,
|
|
40
|
-
{
|
|
40
|
+
{
|
|
41
|
+
runtime: typeof runtime;
|
|
42
|
+
generation: number;
|
|
43
|
+
isRunning: boolean;
|
|
44
|
+
destroy: AbortController;
|
|
45
|
+
}
|
|
41
46
|
>;
|
|
42
47
|
};
|
|
43
48
|
internals.instances.set("thread-1", {
|
|
44
49
|
runtime,
|
|
45
50
|
generation: 0,
|
|
46
51
|
isRunning: false,
|
|
52
|
+
destroy: new AbortController(),
|
|
47
53
|
});
|
|
48
54
|
|
|
49
55
|
const appendPromise = runtime.append({
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
25
25
|
import {
|
|
26
26
|
BaseSubscribable,
|
|
27
|
+
notifySubscribers,
|
|
27
28
|
WritableSubscribable,
|
|
28
29
|
} from "../../subscribable/subscribable";
|
|
29
30
|
import { useSubscribable } from "../../store/runtime-clients/useSubscribable";
|
|
@@ -53,6 +54,9 @@ type RemoteThreadListHookInstance = {
|
|
|
53
54
|
generation: number;
|
|
54
55
|
isRunning: boolean;
|
|
55
56
|
unsubscribeRunning?: Unsubscribe | undefined;
|
|
57
|
+
// Permanent teardown of the thread's runtime, aborted when the thread is
|
|
58
|
+
// stopped or its runtime restarted. A soft unmount leaves it armed.
|
|
59
|
+
destroy: AbortController;
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
type AdapterSnapshot = {
|
|
@@ -61,7 +65,11 @@ type AdapterSnapshot = {
|
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
type HostSnapshot = {
|
|
64
|
-
threads: readonly {
|
|
68
|
+
threads: readonly {
|
|
69
|
+
id: string;
|
|
70
|
+
generation: number;
|
|
71
|
+
destroySignal: AbortSignal;
|
|
72
|
+
}[];
|
|
65
73
|
hookEpoch: number;
|
|
66
74
|
};
|
|
67
75
|
|
|
@@ -129,6 +137,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
129
137
|
this.instances.set(threadId, {
|
|
130
138
|
generation,
|
|
131
139
|
isRunning: false,
|
|
140
|
+
destroy: new AbortController(),
|
|
132
141
|
});
|
|
133
142
|
this._syncHostThreads();
|
|
134
143
|
}
|
|
@@ -141,6 +150,14 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
141
150
|
if (!instance) return this.startThreadRuntime(threadId);
|
|
142
151
|
|
|
143
152
|
if (instance.runtime) invalidateThreadRuntime(instance.runtime);
|
|
153
|
+
// Detach before aborting, as stopThreadRuntime does: the abort runs the
|
|
154
|
+
// destroy listeners synchronously, and a listener that stops the outgoing
|
|
155
|
+
// runtime would otherwise emit that generation's terminal events through
|
|
156
|
+
// the subscription the next generation is about to reuse.
|
|
157
|
+
instance.unsubscribeRunning?.();
|
|
158
|
+
instance.unsubscribeRunning = undefined;
|
|
159
|
+
instance.destroy.abort();
|
|
160
|
+
instance.destroy = new AbortController();
|
|
144
161
|
instance.generation = this.nextGeneration++;
|
|
145
162
|
this._syncHostThreads();
|
|
146
163
|
this._notifySubscribers();
|
|
@@ -202,7 +219,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
202
219
|
}
|
|
203
220
|
this._notifySubscribers();
|
|
204
221
|
if (previousRuntime !== undefined && previousRuntime !== runtime) {
|
|
205
|
-
|
|
222
|
+
notifySubscribers(this.replacedSubscribers);
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
|
|
@@ -254,13 +271,14 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
254
271
|
) {
|
|
255
272
|
if (instance.isRunning === isRunning) return;
|
|
256
273
|
instance.isRunning = isRunning;
|
|
257
|
-
|
|
274
|
+
notifySubscribers(this.runningSubscribers);
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
public stopThreadRuntime(threadId: string) {
|
|
261
278
|
const instance = this.instances.get(threadId);
|
|
262
279
|
if (instance?.runtime) invalidateThreadRuntime(instance.runtime);
|
|
263
280
|
instance?.unsubscribeRunning?.();
|
|
281
|
+
instance?.destroy.abort();
|
|
264
282
|
this.instances.delete(threadId);
|
|
265
283
|
this.pendingThreadAdapters.delete(threadId);
|
|
266
284
|
this._syncHostThreads();
|
|
@@ -302,7 +320,11 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
302
320
|
this.hostStore.setState({
|
|
303
321
|
...host,
|
|
304
322
|
threads: Array.from(this.instances.entries()).map(
|
|
305
|
-
([id, { generation }]) => ({
|
|
323
|
+
([id, { generation, destroy }]) => ({
|
|
324
|
+
id,
|
|
325
|
+
generation,
|
|
326
|
+
destroySignal: destroy.signal,
|
|
327
|
+
}),
|
|
306
328
|
),
|
|
307
329
|
});
|
|
308
330
|
}
|
|
@@ -312,7 +334,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
312
334
|
const adapters = useSubscribable(this.adapterStore);
|
|
313
335
|
const runtimeHook = this.runtimeHook;
|
|
314
336
|
return useResources(
|
|
315
|
-
threads.map(({ id, generation }) => {
|
|
337
|
+
threads.map(({ id, generation, destroySignal }) => {
|
|
316
338
|
const threadAdapters = this.pendingThreadAdapters.has(id)
|
|
317
339
|
? this.pendingThreadAdapters.get(id)!
|
|
318
340
|
: (adapters.threadAdapters.get(id) ?? adapters.defaultAdapters);
|
|
@@ -326,6 +348,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
326
348
|
parentClient,
|
|
327
349
|
adapters: threadAdapters,
|
|
328
350
|
publish: this._publish,
|
|
351
|
+
destroySignal,
|
|
329
352
|
}),
|
|
330
353
|
);
|
|
331
354
|
}),
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { RemoteThreadListResponse } from "../../runtimes/remote-thread-list/types";
|
|
3
|
+
import {
|
|
4
|
+
createCore,
|
|
5
|
+
deferred,
|
|
6
|
+
makeAdapter,
|
|
7
|
+
} from "../../tests/remote-thread-list-test-helpers";
|
|
8
|
+
|
|
9
|
+
const loadedThread = {
|
|
10
|
+
status: "regular" as const,
|
|
11
|
+
remoteId: "thread-1",
|
|
12
|
+
externalId: "thread-1",
|
|
13
|
+
title: "Thread 1",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("RemoteThreadListThreadListRuntimeCore load errors", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps loaded threads and reports a later load failure", async () => {
|
|
22
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
23
|
+
const error = new Error("network error");
|
|
24
|
+
const list = vi
|
|
25
|
+
.fn<() => Promise<RemoteThreadListResponse>>()
|
|
26
|
+
.mockResolvedValueOnce({ threads: [loadedThread] })
|
|
27
|
+
.mockRejectedValueOnce(error);
|
|
28
|
+
const core = createCore(makeAdapter({ list }));
|
|
29
|
+
|
|
30
|
+
await core.getLoadThreadsPromise();
|
|
31
|
+
await core.reload();
|
|
32
|
+
|
|
33
|
+
expect(core.threadIds).toEqual(["thread-1"]);
|
|
34
|
+
expect(core.loadError).toBe(error);
|
|
35
|
+
expect(core.isLoading).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("removes the previous adapter's threads when its replacement load fails", async () => {
|
|
39
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
40
|
+
const error = new Error("replacement failed");
|
|
41
|
+
const core = createCore(
|
|
42
|
+
makeAdapter({
|
|
43
|
+
list: vi.fn(async () => ({ threads: [loadedThread] })),
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
await core.getLoadThreadsPromise();
|
|
47
|
+
|
|
48
|
+
const replacement = makeAdapter({
|
|
49
|
+
list: vi.fn(async () => {
|
|
50
|
+
throw error;
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
core.__internal_setOptions({
|
|
54
|
+
adapter: replacement,
|
|
55
|
+
runtimeHook: () => ({}) as never,
|
|
56
|
+
});
|
|
57
|
+
await core.getLoadThreadsPromise();
|
|
58
|
+
|
|
59
|
+
expect(core.threadIds).toEqual([]);
|
|
60
|
+
expect(core.archivedThreadIds).toEqual([]);
|
|
61
|
+
expect(core.getItemById("thread-1")).toBeUndefined();
|
|
62
|
+
expect(core.loadError).toBe(error);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("clears the previous adapter's error before loading its replacement", async () => {
|
|
66
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
67
|
+
const error = new Error("network error");
|
|
68
|
+
const core = createCore(
|
|
69
|
+
makeAdapter({
|
|
70
|
+
list: vi.fn(async () => {
|
|
71
|
+
throw error;
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
await core.getLoadThreadsPromise();
|
|
76
|
+
expect(core.loadError).toBe(error);
|
|
77
|
+
|
|
78
|
+
const replacementLoad = deferred<RemoteThreadListResponse>();
|
|
79
|
+
const replacementList = vi.fn(() => replacementLoad.promise);
|
|
80
|
+
core.__internal_setOptions({
|
|
81
|
+
adapter: makeAdapter({ list: replacementList }),
|
|
82
|
+
runtimeHook: () => ({}) as never,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(replacementList).not.toHaveBeenCalled();
|
|
86
|
+
expect(core.loadError).toBeUndefined();
|
|
87
|
+
|
|
88
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
89
|
+
replacementLoad.resolve({ threads: [] });
|
|
90
|
+
await loadPromise;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("clears the error when a reload resolves", async () => {
|
|
94
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
95
|
+
const error = new Error("network error");
|
|
96
|
+
const list = vi
|
|
97
|
+
.fn<() => Promise<RemoteThreadListResponse>>()
|
|
98
|
+
.mockRejectedValueOnce(error)
|
|
99
|
+
.mockResolvedValueOnce({ threads: [loadedThread] });
|
|
100
|
+
const core = createCore(makeAdapter({ list }));
|
|
101
|
+
|
|
102
|
+
await core.getLoadThreadsPromise();
|
|
103
|
+
expect(core.loadError).toBe(error);
|
|
104
|
+
|
|
105
|
+
await core.reload();
|
|
106
|
+
|
|
107
|
+
expect(core.loadError).toBeUndefined();
|
|
108
|
+
expect(core.threadIds).toEqual(["thread-1"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("clears the error while a later load is in progress", async () => {
|
|
112
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
113
|
+
const error = new Error("network error");
|
|
114
|
+
const retry = deferred<RemoteThreadListResponse>();
|
|
115
|
+
const list = vi
|
|
116
|
+
.fn<() => Promise<RemoteThreadListResponse>>()
|
|
117
|
+
.mockRejectedValueOnce(error)
|
|
118
|
+
.mockReturnValueOnce(retry.promise);
|
|
119
|
+
const core = createCore(makeAdapter({ list }));
|
|
120
|
+
|
|
121
|
+
await core.getLoadThreadsPromise();
|
|
122
|
+
expect(core.loadError).toBe(error);
|
|
123
|
+
|
|
124
|
+
const retryPromise = core.reload();
|
|
125
|
+
|
|
126
|
+
expect(core.isLoading).toBe(true);
|
|
127
|
+
expect(core.loadError).toBeUndefined();
|
|
128
|
+
|
|
129
|
+
retry.resolve({ threads: [] });
|
|
130
|
+
await retryPromise;
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -120,6 +120,111 @@ describe("RemoteThreadListThreadListRuntimeCore switch/delete ordering", () => {
|
|
|
120
120
|
expect(core.getItemById(core.mainThreadId!)).toBeDefined();
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
it("preserves thread cleanup state when remote deletion fails", async () => {
|
|
124
|
+
const error = new Error("delete failed");
|
|
125
|
+
const adapter = makeAdapter({
|
|
126
|
+
list: vi.fn(async () => ({
|
|
127
|
+
threads: [
|
|
128
|
+
{
|
|
129
|
+
status: "regular" as const,
|
|
130
|
+
remoteId: "thread-b",
|
|
131
|
+
externalId: "thread-b",
|
|
132
|
+
title: "Thread B",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
})),
|
|
136
|
+
delete: vi.fn(async () => {
|
|
137
|
+
throw error;
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
const core = createCore(adapter);
|
|
141
|
+
await core.getLoadThreadsPromise();
|
|
142
|
+
const data = core.getItemById("thread-b")!;
|
|
143
|
+
const internals = core as unknown as {
|
|
144
|
+
_hookManager: { stopThreadRuntime: (id: string) => void };
|
|
145
|
+
_titleStates: Map<string, unknown>;
|
|
146
|
+
};
|
|
147
|
+
const stopThreadRuntime = vi.spyOn(
|
|
148
|
+
internals._hookManager,
|
|
149
|
+
"stopThreadRuntime",
|
|
150
|
+
);
|
|
151
|
+
const titleState = {};
|
|
152
|
+
internals._titleStates.set(data.id, titleState);
|
|
153
|
+
|
|
154
|
+
await expect(core.delete(data.id)).rejects.toBe(error);
|
|
155
|
+
|
|
156
|
+
expect(core.getItemById(data.id)).toBeDefined();
|
|
157
|
+
expect(stopThreadRuntime).not.toHaveBeenCalled();
|
|
158
|
+
expect(internals._titleStates.get(data.id)).toBe(titleState);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("clears the deleted thread's title state on successful deletion", async () => {
|
|
162
|
+
const adapter = makeAdapter({
|
|
163
|
+
list: vi.fn(async () => ({
|
|
164
|
+
threads: [
|
|
165
|
+
{
|
|
166
|
+
status: "regular" as const,
|
|
167
|
+
remoteId: "thread-b",
|
|
168
|
+
externalId: "thread-b",
|
|
169
|
+
title: "Thread B",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
})),
|
|
173
|
+
});
|
|
174
|
+
const core = createCore(adapter);
|
|
175
|
+
await core.getLoadThreadsPromise();
|
|
176
|
+
const data = core.getItemById("thread-b")!;
|
|
177
|
+
const internals = core as unknown as {
|
|
178
|
+
_titleStates: Map<string, unknown>;
|
|
179
|
+
};
|
|
180
|
+
internals._titleStates.set(data.id, {});
|
|
181
|
+
|
|
182
|
+
await core.delete(data.id);
|
|
183
|
+
|
|
184
|
+
expect(core.getItemById(data.id)).toBeUndefined();
|
|
185
|
+
expect(internals._titleStates.has(data.id)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("stops the thread runtime when the adapter changes during a successful deletion", async () => {
|
|
189
|
+
const removal = deferred<void>();
|
|
190
|
+
const adapter = makeAdapter({
|
|
191
|
+
list: vi.fn(async () => ({
|
|
192
|
+
threads: [
|
|
193
|
+
{
|
|
194
|
+
status: "regular" as const,
|
|
195
|
+
remoteId: "thread-b",
|
|
196
|
+
externalId: "thread-b",
|
|
197
|
+
title: "Thread B",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
})),
|
|
201
|
+
delete: vi.fn(() => removal.promise),
|
|
202
|
+
});
|
|
203
|
+
const core = createCore(adapter);
|
|
204
|
+
await core.getLoadThreadsPromise();
|
|
205
|
+
const internals = core as unknown as {
|
|
206
|
+
_hookManager: { stopThreadRuntime: (id: string) => void };
|
|
207
|
+
};
|
|
208
|
+
const stopThreadRuntime = vi.spyOn(
|
|
209
|
+
internals._hookManager,
|
|
210
|
+
"stopThreadRuntime",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const deletion = core.delete("thread-b");
|
|
214
|
+
await vi.waitFor(() => {
|
|
215
|
+
expect(adapter.delete).toHaveBeenCalledWith("thread-b");
|
|
216
|
+
});
|
|
217
|
+
core.__internal_setOptions({
|
|
218
|
+
adapter: makeAdapter(),
|
|
219
|
+
runtimeHook: () => ({}) as never,
|
|
220
|
+
});
|
|
221
|
+
removal.resolve();
|
|
222
|
+
await deletion;
|
|
223
|
+
|
|
224
|
+
expect(core.getItemById("thread-b")).toBeUndefined();
|
|
225
|
+
expect(stopThreadRuntime).toHaveBeenCalledWith("thread-b");
|
|
226
|
+
});
|
|
227
|
+
|
|
123
228
|
it("does not unarchive again when the target became regular during initialization", async () => {
|
|
124
229
|
const initialization = deferred<{
|
|
125
230
|
remoteId: string;
|