@assistant-ui/core 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
- package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
- package/dist/model-context/interactable-composer-metadata.js +259 -0
- package/dist/model-context/interactable-composer-metadata.js.map +1 -0
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/registry.js +1 -0
- package/dist/model-context/registry.js.map +1 -1
- package/dist/model-context/types.d.ts +6 -0
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +4 -0
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/AssistantProvider.d.ts +1 -1
- package/dist/react/AssistantProvider.js +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
- package/dist/react/RuntimeAdapter.js +1 -1
- package/dist/react/client/Interactables.d.ts +8 -2
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +190 -94
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.js +1 -1
- package/dist/react/client/interactable-model-context.d.ts +9 -6
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +154 -32
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/index.d.ts +15 -6
- package/dist/react/index.js +12 -5
- package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
- package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
- package/dist/react/interactables-legacy/Interactables.js +264 -0
- package/dist/react/interactables-legacy/Interactables.js.map +1 -0
- package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
- package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
- package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
- package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
- package/dist/react/interactables-legacy/scopes.d.ts +58 -0
- package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
- package/dist/react/interactables-legacy/scopes.js +0 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
- package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
- package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
- package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
- package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
- package/dist/react/interactables-legacy/useInteractableState.js +84 -0
- package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
- package/dist/react/model-context/interactableTool.d.ts +53 -0
- package/dist/react/model-context/interactableTool.d.ts.map +1 -0
- package/dist/react/model-context/interactableTool.js +188 -0
- package/dist/react/model-context/interactableTool.js.map +1 -0
- package/dist/react/model-context/useAssistantContext.js +1 -1
- package/dist/react/model-context/useAssistantDataUI.js +1 -1
- package/dist/react/model-context/useAssistantInstructions.js +1 -1
- package/dist/react/model-context/useAssistantTool.js +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +1 -1
- package/dist/react/model-context/useAuiToolOverrides.js +1 -1
- package/dist/react/model-context/useInteractable.d.ts +73 -0
- package/dist/react/model-context/useInteractable.d.ts.map +1 -0
- package/dist/react/model-context/useInteractable.js +100 -0
- package/dist/react/model-context/useInteractable.js.map +1 -0
- package/dist/react/model-context/useInteractableState.d.ts +8 -5
- package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
- package/dist/react/model-context/useInteractableState.js +48 -48
- package/dist/react/model-context/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
- package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
- package/dist/react/model-context/useInteractableVersions.js +51 -0
- package/dist/react/model-context/useInteractableVersions.js.map +1 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
- package/dist/react/primitive-hooks/useVoice.js +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.js +1 -1
- package/dist/react/primitives/message/MessageAttachments.js +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
- package/dist/react/primitives/message/MessageParts.js +1 -1
- package/dist/react/primitives/message/MessageQuote.js +1 -1
- package/dist/react/primitives/part/PartMessages.d.ts +5 -2
- package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
- package/dist/react/primitives/part/PartMessages.js +8 -8
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js +64 -2
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
- package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
- package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
- package/dist/react/providers/MessageByIdProvider.js +57 -0
- package/dist/react/providers/MessageByIdProvider.js.map +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +16 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
- package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
- package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
- package/dist/react/runtimes/createRuntimeExtras.js +43 -0
- package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
- package/dist/react/runtimes/useLocalRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
- package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
- package/dist/react/runtimes/useStreamingTiming.js +43 -0
- package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
- package/dist/react/types/scopes/interactables.d.ts +40 -24
- package/dist/react/types/scopes/interactables.d.ts.map +1 -1
- package/dist/react/types/store-augmentation.d.ts +3 -1
- package/dist/react/types/store-augmentation.d.ts.map +1 -1
- package/dist/react/utils/useJSONEqual.d.ts +11 -0
- package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
- package/dist/react/utils/useJSONEqual.js +30 -0
- package/dist/react/utils/useJSONEqual.js.map +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +13 -0
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +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 +4 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.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 +6 -3
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/streaming-timing.d.ts +59 -0
- package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
- package/dist/runtime/utils/streaming-timing.js +76 -0
- package/dist/runtime/utils/streaming-timing.js.map +1 -0
- package/dist/runtime/utils/thread-message-like.js +2 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
- 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 +19 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +1 -1
- package/dist/store/clients/thread-message-client.js +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.js +3 -2
- package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +17 -0
- package/src/internal.ts +7 -0
- package/src/model-context/interactable-composer-metadata.test.ts +550 -0
- package/src/model-context/interactable-composer-metadata.ts +395 -0
- package/src/model-context/registry.ts +5 -0
- package/src/model-context/types.ts +12 -0
- package/src/react/client/Interactables.test.ts +358 -0
- package/src/react/client/Interactables.ts +339 -86
- package/src/react/client/interactable-model-context.test.ts +418 -0
- package/src/react/client/interactable-model-context.ts +252 -58
- package/src/react/index.ts +72 -3
- package/src/react/interactables-legacy/Interactables.ts +332 -0
- package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
- package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
- package/src/react/interactables-legacy/scopes.ts +66 -0
- package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
- package/src/react/interactables-legacy/useInteractableState.ts +67 -0
- package/src/react/model-context/interactableTool.tsx +175 -0
- package/src/react/model-context/useInteractable.ts +233 -0
- package/src/react/model-context/useInteractableState.ts +31 -23
- package/src/react/model-context/useInteractableVersions.ts +48 -0
- package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
- package/src/react/primitives/part/PartMessages.test.tsx +17 -0
- package/src/react/primitives/part/PartMessages.tsx +13 -8
- package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
- package/src/react/providers/MessageByIdProvider.tsx +23 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
- package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
- package/src/react/runtimes/createRuntimeExtras.ts +68 -0
- package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
- package/src/react/runtimes/useStreamingTiming.ts +68 -0
- package/src/react/types/scopes/interactables.ts +45 -23
- package/src/react/types/store-augmentation.ts +4 -2
- package/src/react/utils/useJSONEqual.ts +20 -0
- package/src/runtime/base/base-composer-runtime-core.ts +13 -0
- package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
- package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
- package/src/runtime/utils/message-repository.ts +11 -7
- package/src/runtime/utils/streaming-timing.test.ts +301 -0
- package/src/runtime/utils/streaming-timing.ts +161 -0
- package/src/runtime/utils/thread-message-like.ts +2 -1
- package/src/runtimes/external-store/external-store-adapter.ts +28 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
- package/src/runtimes/remote-thread-list/types.ts +13 -0
- package/src/tests/MessageRepository.test.ts +2 -0
- package/src/tests/composer-can-send.test.ts +1 -0
- package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
- package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
- package/src/tests/groupParts.test.ts +32 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
- package/src/tests/remote-thread-list-test-helpers.ts +2 -1
- package/src/tests/thread-message-like.test.ts +50 -0
- package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
- package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
- package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -92,6 +92,14 @@ export { mergeModelContexts } from "./model-context/types";
|
|
|
92
92
|
|
|
93
93
|
export { tool } from "./model-context/tool";
|
|
94
94
|
|
|
95
|
+
export {
|
|
96
|
+
unstable_getInteractableSnapshots,
|
|
97
|
+
unstable_formatInteractableSnapshot,
|
|
98
|
+
unstable_getInteractableVersions,
|
|
99
|
+
type Unstable_InteractableSnapshotEntry,
|
|
100
|
+
type Unstable_InteractableVersion,
|
|
101
|
+
} from "./model-context/interactable-composer-metadata";
|
|
102
|
+
|
|
95
103
|
export type { ToolExecutionStatus } from "./runtimes/tool-invocations/ToolInvocationTracker";
|
|
96
104
|
|
|
97
105
|
export { ModelContextRegistry } from "./model-context/registry";
|
|
@@ -267,6 +275,14 @@ export type {
|
|
|
267
275
|
export type { ThreadMessageLike } from "./runtime/utils/thread-message-like";
|
|
268
276
|
export { fromThreadMessageLike } from "./runtime/utils/thread-message-like";
|
|
269
277
|
|
|
278
|
+
// Streaming timing (client-side)
|
|
279
|
+
export type {
|
|
280
|
+
StreamingTimingAccessors,
|
|
281
|
+
StreamingTimingOptions,
|
|
282
|
+
StreamingTimingState,
|
|
283
|
+
} from "./runtime/utils/streaming-timing";
|
|
284
|
+
export { stepStreamingTiming } from "./runtime/utils/streaming-timing";
|
|
285
|
+
|
|
270
286
|
// ID generation
|
|
271
287
|
export { generateId } from "./utils/id";
|
|
272
288
|
|
|
@@ -289,6 +305,7 @@ export type {
|
|
|
289
305
|
ExternalStoreMessageConverter,
|
|
290
306
|
ExternalStoreThreadListAdapter,
|
|
291
307
|
ExternalStoreThreadData,
|
|
308
|
+
ExternalStoreBranchChange,
|
|
292
309
|
} from "./runtimes/external-store/external-store-adapter";
|
|
293
310
|
export type { ExternalStoreSharedOptions } from "./runtimes/external-store/external-store-shared-options";
|
|
294
311
|
export { pickExternalStoreSharedOptions } from "./runtimes/external-store/external-store-shared-options";
|
package/src/internal.ts
CHANGED
|
@@ -37,5 +37,12 @@ export { resolveToolApprovalResponse } from "./runtime/utils/resolveToolApproval
|
|
|
37
37
|
// Composite context provider
|
|
38
38
|
export { CompositeContextProvider } from "./utils/composite-context-provider";
|
|
39
39
|
|
|
40
|
+
// Runtime extras helper for external-store adapters. Internal because the
|
|
41
|
+
// tap-native runtime path replaces the `thread.extras` side-channel it wraps.
|
|
42
|
+
export {
|
|
43
|
+
createRuntimeExtras,
|
|
44
|
+
type RuntimeExtras,
|
|
45
|
+
} from "./react/runtimes/createRuntimeExtras";
|
|
46
|
+
|
|
40
47
|
export * from "./runtime/internal";
|
|
41
48
|
export * from "./runtimes/internal";
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
findModelKnownState,
|
|
4
|
+
unstable_formatInteractableSnapshot,
|
|
5
|
+
gateInteractableComposerMetadata,
|
|
6
|
+
unstable_getInteractableSnapshots,
|
|
7
|
+
unstable_getInteractableVersions,
|
|
8
|
+
interactableToolName,
|
|
9
|
+
shallowMergeInteractableState,
|
|
10
|
+
type Unstable_InteractableSnapshotEntry,
|
|
11
|
+
} from "./interactable-composer-metadata";
|
|
12
|
+
|
|
13
|
+
const userMsg = (interactables: Unstable_InteractableSnapshotEntry[]) => ({
|
|
14
|
+
role: "user" as const,
|
|
15
|
+
metadata: { custom: { interactables } },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const assistantMsg = (interactables: Unstable_InteractableSnapshotEntry[]) => ({
|
|
19
|
+
role: "assistant" as const,
|
|
20
|
+
metadata: { custom: { interactables } },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const toolCallPart = (
|
|
24
|
+
toolName: string,
|
|
25
|
+
args: Record<string, unknown>,
|
|
26
|
+
result?: unknown,
|
|
27
|
+
toolCallId?: string,
|
|
28
|
+
) => ({
|
|
29
|
+
type: "tool-call" as const,
|
|
30
|
+
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
31
|
+
toolName,
|
|
32
|
+
args,
|
|
33
|
+
...(result !== undefined ? { result } : {}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const assistantToolCall = (
|
|
37
|
+
toolName: string,
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
result?: unknown,
|
|
40
|
+
toolCallId?: string,
|
|
41
|
+
) => ({
|
|
42
|
+
role: "assistant" as const,
|
|
43
|
+
content: [toolCallPart(toolName, args, result, toolCallId)],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The assistant message that created instance `id` via a tool call. */
|
|
47
|
+
const assistantCreateCall = (id: string, args: Record<string, unknown>) =>
|
|
48
|
+
assistantToolCall("note", args, { success: true }, id);
|
|
49
|
+
|
|
50
|
+
const entry = (
|
|
51
|
+
id: string,
|
|
52
|
+
state: unknown,
|
|
53
|
+
name = id,
|
|
54
|
+
): Unstable_InteractableSnapshotEntry => ({ id, name, state });
|
|
55
|
+
|
|
56
|
+
const partialEntry = (
|
|
57
|
+
id: string,
|
|
58
|
+
state: unknown,
|
|
59
|
+
name = id,
|
|
60
|
+
): Unstable_InteractableSnapshotEntry => ({ id, name, state, partial: true });
|
|
61
|
+
|
|
62
|
+
describe("unstable_getInteractableSnapshots", () => {
|
|
63
|
+
it("reads entries from metadata.custom.interactables", () => {
|
|
64
|
+
const msg = userMsg([entry("a", { v: 1 })]);
|
|
65
|
+
expect(unstable_getInteractableSnapshots(msg)).toEqual([
|
|
66
|
+
entry("a", { v: 1 }),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns undefined for missing or malformed metadata", () => {
|
|
71
|
+
expect(unstable_getInteractableSnapshots({})).toBeUndefined();
|
|
72
|
+
expect(
|
|
73
|
+
unstable_getInteractableSnapshots({ metadata: null }),
|
|
74
|
+
).toBeUndefined();
|
|
75
|
+
expect(
|
|
76
|
+
unstable_getInteractableSnapshots({
|
|
77
|
+
metadata: { custom: { interactables: "x" } },
|
|
78
|
+
}),
|
|
79
|
+
).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("unstable_formatInteractableSnapshot", () => {
|
|
84
|
+
it("includes the name, id, and JSON state", () => {
|
|
85
|
+
expect(
|
|
86
|
+
unstable_formatInteractableSnapshot(entry("n1", { v: 1 }, "note")),
|
|
87
|
+
).toBe('[Current state of "note" (id: "n1"): {"v":1}]');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("formats a partial snapshot as changed fields", () => {
|
|
91
|
+
expect(
|
|
92
|
+
unstable_formatInteractableSnapshot(partialEntry("n1", { v: 1 }, "note")),
|
|
93
|
+
).toBe(
|
|
94
|
+
'[State of "note" (id: "n1") changed — updated fields: {"v":1}; fields not listed are unchanged]',
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("interactableToolName", () => {
|
|
100
|
+
it("sanitizes the name", () => {
|
|
101
|
+
expect(interactableToolName("note")).toBe("update_note");
|
|
102
|
+
expect(interactableToolName("my notes!")).toBe("update_my_notes_");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("shallowMergeInteractableState", () => {
|
|
107
|
+
it("applies array operations from a baseline", () => {
|
|
108
|
+
const prev = {
|
|
109
|
+
tasks: [
|
|
110
|
+
{ id: "a", title: "A", done: false },
|
|
111
|
+
{ id: "b", title: "B", done: false },
|
|
112
|
+
],
|
|
113
|
+
selectedId: "a",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(
|
|
117
|
+
shallowMergeInteractableState(prev, {
|
|
118
|
+
tasks: {
|
|
119
|
+
update: [{ id: "a", done: true }],
|
|
120
|
+
remove: ["b"],
|
|
121
|
+
add: [{ id: "c", title: "C", done: false }],
|
|
122
|
+
},
|
|
123
|
+
selectedId: "c",
|
|
124
|
+
}),
|
|
125
|
+
).toEqual({
|
|
126
|
+
tasks: [
|
|
127
|
+
{ id: "a", title: "A", done: true },
|
|
128
|
+
{ id: "c", title: "C", done: false },
|
|
129
|
+
],
|
|
130
|
+
selectedId: "c",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keeps raw array replacement semantics", () => {
|
|
135
|
+
expect(
|
|
136
|
+
shallowMergeInteractableState(
|
|
137
|
+
{ tasks: [{ id: "a", title: "A" }] },
|
|
138
|
+
{ tasks: [{ id: "b", title: "B" }] },
|
|
139
|
+
),
|
|
140
|
+
).toEqual({ tasks: [{ id: "b", title: "B" }] });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("uses the array baseline only for array operation fields", () => {
|
|
144
|
+
expect(
|
|
145
|
+
shallowMergeInteractableState(
|
|
146
|
+
{ tasks: [{ id: "stream", title: "streamed" }], title: "live" },
|
|
147
|
+
{ tasks: { add: [{ id: "final", title: "final" }] }, title: "final" },
|
|
148
|
+
{ arrayBaseline: { tasks: [{ id: "base", title: "base" }] } },
|
|
149
|
+
),
|
|
150
|
+
).toEqual({
|
|
151
|
+
tasks: [
|
|
152
|
+
{ id: "base", title: "base" },
|
|
153
|
+
{ id: "final", title: "final" },
|
|
154
|
+
],
|
|
155
|
+
title: "final",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("findModelKnownState", () => {
|
|
161
|
+
it("returns undefined when no snapshot or creating call exists", () => {
|
|
162
|
+
const history = [assistantToolCall("update_note", { id: "a", v: 9 })];
|
|
163
|
+
expect(findModelKnownState(history, "a", "note")).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns the latest snapshot when no later tool calls exist", () => {
|
|
167
|
+
const history = [userMsg([entry("a", { v: 1 }, "note")])];
|
|
168
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({ v: 1 });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("folds the assistant's own update_* calls on top of the snapshot", () => {
|
|
172
|
+
const history = [
|
|
173
|
+
userMsg([entry("a", { v: 1, title: "x" }, "note")]),
|
|
174
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
175
|
+
];
|
|
176
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({
|
|
177
|
+
v: 2,
|
|
178
|
+
title: "x",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("lets a later full snapshot replace earlier folded state", () => {
|
|
183
|
+
const history = [
|
|
184
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
185
|
+
assistantToolCall("update_note", { id: "a", v: 2 }),
|
|
186
|
+
userMsg([entry("a", { v: 10 }, "note")]),
|
|
187
|
+
];
|
|
188
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({
|
|
189
|
+
v: 10,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("merges partial snapshots into the known state", () => {
|
|
194
|
+
const history = [
|
|
195
|
+
userMsg([entry("a", { v: 1, title: "x" }, "note")]),
|
|
196
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
197
|
+
userMsg([partialEntry("a", { title: "y" }, "note")]),
|
|
198
|
+
];
|
|
199
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({
|
|
200
|
+
v: 2,
|
|
201
|
+
title: "y",
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("ignores a partial snapshot without a baseline", () => {
|
|
206
|
+
const history = [userMsg([partialEntry("a", { title: "y" }, "note")])];
|
|
207
|
+
expect(findModelKnownState(history, "a", "note")).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("seeds the baseline from the creating call's args (id === toolCallId)", () => {
|
|
211
|
+
const history = [assistantCreateCall("a", { v: 1, title: "draft" })];
|
|
212
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({
|
|
213
|
+
v: 1,
|
|
214
|
+
title: "draft",
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("folds update_* calls on top of the creating call's args", () => {
|
|
219
|
+
const history = [
|
|
220
|
+
assistantCreateCall("a", { v: 1, title: "draft" }),
|
|
221
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
222
|
+
];
|
|
223
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({
|
|
224
|
+
v: 2,
|
|
225
|
+
title: "draft",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("ignores calls targeting another instance id", () => {
|
|
230
|
+
const history = [
|
|
231
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
232
|
+
assistantToolCall("update_note", { id: "b", v: 2 }),
|
|
233
|
+
];
|
|
234
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({ v: 1 });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("ignores calls of other tools", () => {
|
|
238
|
+
const history = [
|
|
239
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
240
|
+
assistantToolCall("update_board", { id: "a", v: 2 }),
|
|
241
|
+
];
|
|
242
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({ v: 1 });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("ignores rejected calls (success: false results never reached the client)", () => {
|
|
246
|
+
const history = [
|
|
247
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
248
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: false }),
|
|
249
|
+
];
|
|
250
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({ v: 1 });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("folds id-less calls (accepted while a single instance exists)", () => {
|
|
254
|
+
const history = [
|
|
255
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
256
|
+
assistantToolCall("update_note", { v: 2 }),
|
|
257
|
+
];
|
|
258
|
+
expect(findModelKnownState(history, "a", "note")?.state).toEqual({ v: 2 });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("replays assigned ids from array add update results", () => {
|
|
262
|
+
const history = [
|
|
263
|
+
userMsg([entry("board", { tasks: [] }, "taskBoard")]),
|
|
264
|
+
assistantToolCall(
|
|
265
|
+
"update_taskBoard",
|
|
266
|
+
{ id: "board", tasks: { add: [{ title: "Write tests" }] } },
|
|
267
|
+
{
|
|
268
|
+
success: true,
|
|
269
|
+
id: "board",
|
|
270
|
+
addedItemIds: { tasks: ["task-1"] },
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
];
|
|
274
|
+
expect(findModelKnownState(history, "board", "taskBoard")?.state).toEqual({
|
|
275
|
+
tasks: [{ title: "Write tests", id: "task-1" }],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("unstable_getInteractableVersions", () => {
|
|
281
|
+
it("returns an empty list when nothing references the instance", () => {
|
|
282
|
+
expect(unstable_getInteractableVersions([], "a", "note")).toEqual([]);
|
|
283
|
+
expect(
|
|
284
|
+
unstable_getInteractableVersions(
|
|
285
|
+
[assistantToolCall("update_note", { id: "b", v: 1 }, undefined, "x")],
|
|
286
|
+
"a",
|
|
287
|
+
"note",
|
|
288
|
+
),
|
|
289
|
+
).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("records create, user-edit, and update versions in order", () => {
|
|
293
|
+
const history = [
|
|
294
|
+
assistantCreateCall("a", { v: 1, title: "draft" }),
|
|
295
|
+
userMsg([partialEntry("a", { title: "edited" }, "note")]),
|
|
296
|
+
assistantToolCall(
|
|
297
|
+
"update_note",
|
|
298
|
+
{ id: "a", v: 2 },
|
|
299
|
+
{ success: true },
|
|
300
|
+
"call-2",
|
|
301
|
+
),
|
|
302
|
+
];
|
|
303
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toEqual([
|
|
304
|
+
{ state: { v: 1, title: "draft" }, origin: "create", toolCallId: "a" },
|
|
305
|
+
{ state: { v: 1, title: "edited" }, origin: "user-edit" },
|
|
306
|
+
{
|
|
307
|
+
state: { v: 2, title: "edited" },
|
|
308
|
+
origin: "update",
|
|
309
|
+
toolCallId: "call-2",
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("replaces the state on a full user snapshot", () => {
|
|
315
|
+
const history = [
|
|
316
|
+
assistantCreateCall("a", { v: 1, title: "draft" }),
|
|
317
|
+
userMsg([entry("a", { v: 10 }, "note")]),
|
|
318
|
+
];
|
|
319
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toEqual([
|
|
320
|
+
{ state: { v: 1, title: "draft" }, origin: "create", toolCallId: "a" },
|
|
321
|
+
{ state: { v: 10 }, origin: "user-edit" },
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("attributes an id-less update via the result's resolved id", () => {
|
|
326
|
+
const history = [
|
|
327
|
+
assistantCreateCall("a", { v: 1 }),
|
|
328
|
+
assistantToolCall(
|
|
329
|
+
"update_note",
|
|
330
|
+
{ v: 2 },
|
|
331
|
+
{ success: true, id: "a" },
|
|
332
|
+
"call-2",
|
|
333
|
+
),
|
|
334
|
+
];
|
|
335
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toEqual([
|
|
336
|
+
{ state: { v: 1 }, origin: "create", toolCallId: "a" },
|
|
337
|
+
{ state: { v: 2 }, origin: "update", toolCallId: "call-2" },
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("excludes an id-less update resolved to another instance", () => {
|
|
342
|
+
const history = [
|
|
343
|
+
assistantCreateCall("a", { v: 1 }),
|
|
344
|
+
assistantToolCall(
|
|
345
|
+
"update_note",
|
|
346
|
+
{ v: 2 },
|
|
347
|
+
{ success: true, id: "b" },
|
|
348
|
+
"call-2",
|
|
349
|
+
),
|
|
350
|
+
];
|
|
351
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toHaveLength(
|
|
352
|
+
1,
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("skips rejected update calls", () => {
|
|
357
|
+
const history = [
|
|
358
|
+
assistantCreateCall("a", { v: 1 }),
|
|
359
|
+
assistantToolCall(
|
|
360
|
+
"update_note",
|
|
361
|
+
{ id: "a", v: 2 },
|
|
362
|
+
{ success: false },
|
|
363
|
+
"call-2",
|
|
364
|
+
),
|
|
365
|
+
];
|
|
366
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toEqual([
|
|
367
|
+
{ state: { v: 1 }, origin: "create", toolCallId: "a" },
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("skips partial snapshots and updates that have no baseline", () => {
|
|
372
|
+
const history = [
|
|
373
|
+
userMsg([partialEntry("a", { title: "y" }, "note")]),
|
|
374
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
375
|
+
];
|
|
376
|
+
expect(unstable_getInteractableVersions(history, "a", "note")).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("gateInteractableComposerMetadata", () => {
|
|
381
|
+
it("returns undefined when meta is undefined", () => {
|
|
382
|
+
expect(gateInteractableComposerMetadata(undefined, [])).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("stamps a full baseline on the first send (no prior record in history)", () => {
|
|
386
|
+
const meta = { interactables: [entry("a", { v: 1 })] };
|
|
387
|
+
const gated = gateInteractableComposerMetadata(meta, []);
|
|
388
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 1 })]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("is idempotent: omits an interactable whose state matches its latest snapshot", () => {
|
|
392
|
+
const meta = { interactables: [entry("a", { v: 1 })] };
|
|
393
|
+
const history = [userMsg([entry("a", { v: 1 })])];
|
|
394
|
+
expect(gateInteractableComposerMetadata(meta, history)).toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("omits an unedited tool-created interactable (the model authored its args)", () => {
|
|
398
|
+
const meta = {
|
|
399
|
+
interactables: [entry("a", { v: 1, title: "draft" }, "note")],
|
|
400
|
+
};
|
|
401
|
+
const history = [assistantCreateCall("a", { v: 1, title: "draft" })];
|
|
402
|
+
expect(gateInteractableComposerMetadata(meta, history)).toBeUndefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("stamps the full state when every field changed", () => {
|
|
406
|
+
const meta = { interactables: [entry("a", { v: 2 })] };
|
|
407
|
+
const history = [userMsg([entry("a", { v: 1 })])];
|
|
408
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
409
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 2 })]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("stamps only the changed fields as a partial snapshot", () => {
|
|
413
|
+
const meta = { interactables: [entry("a", { v: 1, title: "edited" })] };
|
|
414
|
+
const history = [userMsg([entry("a", { v: 1, title: "draft" })])];
|
|
415
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
416
|
+
expect(gated?.interactables).toEqual([
|
|
417
|
+
partialEntry("a", { title: "edited" }),
|
|
418
|
+
]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("falls back to a full snapshot when a top-level key was removed", () => {
|
|
422
|
+
const meta = { interactables: [entry("a", { v: 1 })] };
|
|
423
|
+
const history = [userMsg([entry("a", { v: 1, title: "draft" })])];
|
|
424
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
425
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 1 })]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("omits an interactable the model already knows via its own update_* call", () => {
|
|
429
|
+
const meta = { interactables: [entry("a", { v: 2 }, "note")] };
|
|
430
|
+
const history = [
|
|
431
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
432
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
433
|
+
];
|
|
434
|
+
expect(gateInteractableComposerMetadata(meta, history)).toBeUndefined();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("omits array add state when update results include assigned ids", () => {
|
|
438
|
+
const meta = {
|
|
439
|
+
interactables: [
|
|
440
|
+
entry(
|
|
441
|
+
"board",
|
|
442
|
+
{ tasks: [{ title: "Write tests", id: "task-1" }] },
|
|
443
|
+
"taskBoard",
|
|
444
|
+
),
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
const history = [
|
|
448
|
+
userMsg([entry("board", { tasks: [] }, "taskBoard")]),
|
|
449
|
+
assistantToolCall(
|
|
450
|
+
"update_taskBoard",
|
|
451
|
+
{ id: "board", tasks: { add: [{ title: "Write tests" }] } },
|
|
452
|
+
{
|
|
453
|
+
success: true,
|
|
454
|
+
id: "board",
|
|
455
|
+
addedItemIds: { tasks: ["task-1"] },
|
|
456
|
+
},
|
|
457
|
+
),
|
|
458
|
+
];
|
|
459
|
+
expect(gateInteractableComposerMetadata(meta, history)).toBeUndefined();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("stamps when the user edited after the model's last update_* call", () => {
|
|
463
|
+
const meta = { interactables: [entry("a", { v: 3 }, "note")] };
|
|
464
|
+
const history = [
|
|
465
|
+
userMsg([entry("a", { v: 1 }, "note")]),
|
|
466
|
+
assistantToolCall("update_note", { id: "a", v: 2 }, { success: true }),
|
|
467
|
+
];
|
|
468
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
469
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 3 }, "note")]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("re-stamps a revert-to-initial-state because the gate compares to history, not the seed", () => {
|
|
473
|
+
// initialState was { v: 0 }; the latest snapshot moved it to { v: 1 };
|
|
474
|
+
// reverting the live state back to { v: 0 } still differs from history,
|
|
475
|
+
// so it must re-stamp (proves the gate never references initialState).
|
|
476
|
+
const meta = { interactables: [entry("a", { v: 0 })] };
|
|
477
|
+
const history = [userMsg([entry("a", { v: 1 })])];
|
|
478
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
479
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 0 })]);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("includes only the interactables that changed", () => {
|
|
483
|
+
const meta = {
|
|
484
|
+
interactables: [entry("a", { v: 1 }), entry("b", { v: 99 })],
|
|
485
|
+
};
|
|
486
|
+
const history = [userMsg([entry("a", { v: 1 }), entry("b", { v: 2 })])];
|
|
487
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
488
|
+
expect(gated?.interactables).toEqual([entry("b", { v: 99 })]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("skips non-user snapshot carriers when folding", () => {
|
|
492
|
+
const meta = { interactables: [entry("a", { v: 999 })] };
|
|
493
|
+
const history = [
|
|
494
|
+
userMsg([entry("a", { v: 1 })]),
|
|
495
|
+
assistantMsg([entry("a", { v: 999 })]),
|
|
496
|
+
];
|
|
497
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
498
|
+
expect(gated?.interactables).toEqual([entry("a", { v: 999 })]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("passes through non-interactable metadata keys untouched", () => {
|
|
502
|
+
const meta = { interactables: [entry("a", { v: 1 })], foo: "bar" };
|
|
503
|
+
const history = [userMsg([entry("a", { v: 1 })])];
|
|
504
|
+
const gated = gateInteractableComposerMetadata(meta, history);
|
|
505
|
+
expect(gated).toEqual({ foo: "bar" });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("non-JSON state", () => {
|
|
509
|
+
const ORIGINAL_ENV = process.env.NODE_ENV;
|
|
510
|
+
afterEach(() => {
|
|
511
|
+
process.env.NODE_ENV = ORIGINAL_ENV;
|
|
512
|
+
vi.restoreAllMocks();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("warns in dev and re-stamps every send (state is not JSON-equatable)", () => {
|
|
516
|
+
process.env.NODE_ENV = "development";
|
|
517
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
518
|
+
|
|
519
|
+
// A field set to `undefined` makes the value fail `isJSONValue`, so it is
|
|
520
|
+
// never equal to itself — the gate can't dedupe it against history.
|
|
521
|
+
const nonJson = { note: undefined };
|
|
522
|
+
const meta = { interactables: [entry("a", nonJson)] };
|
|
523
|
+
|
|
524
|
+
// First send: no history, stamps.
|
|
525
|
+
const first = gateInteractableComposerMetadata(meta, []);
|
|
526
|
+
expect(first?.interactables).toHaveLength(1);
|
|
527
|
+
|
|
528
|
+
// Second send: history already carries the snapshot, yet the value never
|
|
529
|
+
// compares equal, so it re-stamps — documenting per-message growth.
|
|
530
|
+
const history = [
|
|
531
|
+
userMsg(first!.interactables as Unstable_InteractableSnapshotEntry[]),
|
|
532
|
+
];
|
|
533
|
+
const second = gateInteractableComposerMetadata(meta, history);
|
|
534
|
+
expect(second?.interactables).toHaveLength(1);
|
|
535
|
+
|
|
536
|
+
expect(warn).toHaveBeenCalled();
|
|
537
|
+
expect(warn.mock.calls[0]![0]).toContain("not JSON-equatable");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("does not warn in production", () => {
|
|
541
|
+
process.env.NODE_ENV = "production";
|
|
542
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
543
|
+
|
|
544
|
+
const meta = { interactables: [entry("a", { note: undefined })] };
|
|
545
|
+
gateInteractableComposerMetadata(meta, []);
|
|
546
|
+
|
|
547
|
+
expect(warn).not.toHaveBeenCalled();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|