@runtypelabs/persona 4.17.0 → 4.18.0
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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-BpWGm7_6.d.cts → types-DsmTD8TO.d.cts} +12 -0
- package/dist/animations/{types-BpWGm7_6.d.ts → types-DsmTD8TO.d.ts} +12 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-3I2S3I5P.js +1 -0
- package/dist/chunk-76IELU7F.js +1 -0
- package/dist/chunk-EFJBUH5F.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-Q735PSZL.js +1 -0
- package/dist/chunk-VK6JOIYJ.js +1 -0
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +4 -4
- package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
- package/dist/context-mentions.cjs +9 -8
- package/dist/context-mentions.d.cts +1224 -14
- package/dist/context-mentions.d.ts +1224 -14
- package/dist/context-mentions.js +9 -8
- package/dist/event-stream-view-4EZ7BGFK.js +1 -0
- package/dist/event-stream-view.cjs +129 -0
- package/dist/event-stream-view.d.cts +8384 -0
- package/dist/event-stream-view.d.ts +8384 -0
- package/dist/event-stream-view.js +129 -0
- package/dist/history-view-BD6OXLNA.js +898 -0
- package/dist/history-view.cjs +898 -0
- package/dist/history-view.d.cts +1461 -0
- package/dist/history-view.d.ts +1461 -0
- package/dist/history-view.js +898 -0
- package/dist/index.cjs +77 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2040 -21
- package/dist/index.d.ts +2040 -21
- package/dist/index.global.js +67 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +77 -77
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -4
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +0 -8
- package/dist/plugin-kit.d.ts +0 -8
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.d.cts +1224 -14
- package/dist/smart-dom-reader.d.ts +1224 -14
- package/dist/theme-editor-preview.cjs +970 -73
- package/dist/theme-editor-preview.d.cts +1343 -15
- package/dist/theme-editor-preview.d.ts +1343 -15
- package/dist/theme-editor-preview.js +81 -81
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +1227 -15
- package/dist/theme-editor.d.ts +1227 -15
- package/dist/theme-editor.js +9 -9
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +226 -7
- package/dist/theme-reference.d.ts +226 -7
- package/dist/theme-reference.js +1 -1
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +18 -6
- package/src/client-history-rest.test.ts +1034 -0
- package/src/client-visitor-history.test.ts +596 -0
- package/src/client.ts +1040 -24
- package/src/components/artifact-inline.ts +8 -1
- package/src/components/artifact-pane.test.ts +278 -1
- package/src/components/artifact-pane.ts +135 -67
- package/src/components/artifact-preview.test.ts +14 -1
- package/src/components/artifact-preview.ts +9 -0
- package/src/components/header-builder.ts +3 -0
- package/src/components/header-layouts.test.ts +171 -7
- package/src/components/header-layouts.ts +83 -53
- package/src/components/header-parts.test.ts +154 -1
- package/src/components/header-parts.ts +266 -138
- package/src/components/history-confirm-dialog.ts +177 -0
- package/src/components/history-shell-copy.ts +99 -0
- package/src/components/history-view/announcer.ts +40 -0
- package/src/components/history-view/copy.ts +105 -0
- package/src/components/history-view/css.ts +1078 -0
- package/src/components/history-view/grouping.test.ts +142 -0
- package/src/components/history-view/grouping.ts +142 -0
- package/src/components/history-view/icons.ts +73 -0
- package/src/components/history-view/rows.ts +341 -0
- package/src/components/history-view/state.ts +309 -0
- package/src/components/history-view.test.ts +2660 -0
- package/src/components/history-view.ts +1891 -0
- package/src/components/launcher-surface.test.ts +133 -0
- package/src/components/launcher.test.ts +18 -0
- package/src/components/launcher.ts +7 -7
- package/src/components/panel.ts +17 -7
- package/src/components/welcome.ts +72 -2
- package/src/defaults.ts +36 -3
- package/src/event-stream-view-bundle.test.ts +69 -0
- package/src/event-stream-view-entry.ts +19 -0
- package/src/event-stream-view-loader.ts +31 -0
- package/src/event-stream-view.ts +19 -0
- package/src/generated/runtype-openapi-contract.ts +54 -4
- package/src/history-render.ts +325 -0
- package/src/history-view-bundle.test.ts +78 -0
- package/src/history-view-entry.ts +41 -0
- package/src/history-view-loader.test.ts +30 -0
- package/src/history-view-loader.ts +32 -0
- package/src/history-view.ts +28 -0
- package/src/index-core.ts +50 -2
- package/src/index-global.ts +55 -0
- package/src/internal/demo-history-provider.test.ts +402 -0
- package/src/internal/demo-history-provider.ts +689 -0
- package/src/internal/history-provider-registry.ts +25 -0
- package/src/internal/history-provider.ts +150 -0
- package/src/internal/runtype-history-provider.test.ts +313 -0
- package/src/internal/runtype-history-provider.ts +240 -0
- package/src/plugin-kit.test.ts +16 -0
- package/src/plugin-kit.ts +13 -5
- package/src/plugins/types.ts +177 -1
- package/src/session-history.test.ts +1397 -0
- package/src/session.ts +1200 -26
- package/src/styles/widget-styles.test.ts +133 -0
- package/src/styles/widget.css +227 -8
- package/src/theme-editor/preview-utils.test.ts +19 -0
- package/src/theme-editor/preview-utils.ts +15 -1
- package/src/theme-editor/preview.ts +7 -1
- package/src/theme-editor/role-mappings.ts +4 -1
- package/src/theme-editor/sections.test.ts +168 -1
- package/src/theme-editor/sections.ts +136 -7
- package/src/theme-editor/webmcp/summary.ts +16 -2
- package/src/theme-reference.ts +20 -6
- package/src/types/theme.ts +213 -7
- package/src/types.ts +927 -7
- package/src/ui.artifact-rail-resize.test.ts +235 -0
- package/src/ui.attachments-drop.test.ts +1 -0
- package/src/ui.header-update-stability.test.ts +2 -2
- package/src/ui.history-chunk.test.ts +72 -0
- package/src/ui.history-config-provider.test.ts +318 -0
- package/src/ui.history-controller.test.ts +711 -0
- package/src/ui.history-lifecycle.test.ts +456 -0
- package/src/ui.history-render-hooks.test.ts +904 -0
- package/src/ui.history-shell.test.ts +1887 -0
- package/src/ui.rail-overlay.test.ts +573 -0
- package/src/ui.rail-resize.test.ts +284 -0
- package/src/ui.rebuild-composer.test.ts +21 -0
- package/src/ui.render-welcome.test.ts +42 -0
- package/src/ui.shortcuts.test.ts +166 -0
- package/src/ui.ts +3071 -240
- package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
- package/src/utils/artifact-file.ts +15 -0
- package/src/utils/artifact-gate.test.ts +4 -0
- package/src/utils/artifact-gate.ts +13 -17
- package/src/utils/artifact-resize.test.ts +28 -0
- package/src/utils/artifact-resize.ts +9 -2
- package/src/utils/buttons.ts +188 -1
- package/src/utils/config-merge.test.ts +91 -0
- package/src/utils/dropdown.ts +10 -1
- package/src/utils/history-messages.test.ts +284 -0
- package/src/utils/history-messages.ts +284 -0
- package/src/utils/icons.ts +3 -0
- package/src/utils/shortcuts.test.ts +288 -0
- package/src/utils/shortcuts.ts +221 -0
- package/src/utils/theme.test.ts +501 -4
- package/src/utils/theme.ts +42 -3
- package/src/utils/tokens.ts +153 -15
- package/src/utils/tooltip-timing.ts +4 -0
- package/src/utils/tooltip.test.ts +249 -7
- package/src/utils/tooltip.ts +146 -4
- package/src/utils/visitor-store.test.ts +425 -0
- package/src/utils/visitor-store.ts +337 -0
- package/dist/chunk-QJ4LV2KW.js +0 -1
package/src/index-core.ts
CHANGED
|
@@ -32,6 +32,7 @@ export type {
|
|
|
32
32
|
AgentWidgetMessage,
|
|
33
33
|
AgentWidgetLauncherConfig,
|
|
34
34
|
AgentWidgetLauncherTeaserConfig,
|
|
35
|
+
AgentWidgetTooltipConfig,
|
|
35
36
|
AgentWidgetDockConfig,
|
|
36
37
|
AgentWidgetEvent,
|
|
37
38
|
AgentWidgetStreamParser,
|
|
@@ -93,6 +94,7 @@ export type {
|
|
|
93
94
|
// Client token types
|
|
94
95
|
ClientSession,
|
|
95
96
|
ClientInitResponse,
|
|
97
|
+
PreparedClientSession,
|
|
96
98
|
ClientChatRequest,
|
|
97
99
|
ClientFeedbackRequest,
|
|
98
100
|
ClientFeedbackType,
|
|
@@ -142,6 +144,30 @@ export type {
|
|
|
142
144
|
AskUserQuestionOption,
|
|
143
145
|
AgentWidgetAskUserQuestionFeature,
|
|
144
146
|
AgentWidgetAskUserQuestionStyles,
|
|
147
|
+
// Visitor conversation history (client-token mode) types
|
|
148
|
+
AgentWidgetHistoryFeature,
|
|
149
|
+
AgentWidgetHistoryListAction,
|
|
150
|
+
AgentWidgetHistoryCopy,
|
|
151
|
+
HistoryScope,
|
|
152
|
+
HistoryIdentityStatus,
|
|
153
|
+
ResolvedHistoryPresentation,
|
|
154
|
+
HistoryReturnSurface,
|
|
155
|
+
HistoryConversationSummary,
|
|
156
|
+
HistoryConversationPatch,
|
|
157
|
+
HistoryConversationPage,
|
|
158
|
+
HistoryConversationDetail,
|
|
159
|
+
HistoryDisplayProjection,
|
|
160
|
+
// History rendering customization (plugin render hooks; types only)
|
|
161
|
+
ResolvedHistoryCopy,
|
|
162
|
+
HistoryViewState,
|
|
163
|
+
HistoryPendingAction,
|
|
164
|
+
AgentWidgetHistoryRenderActions,
|
|
165
|
+
AgentWidgetRenderHistoryViewContext,
|
|
166
|
+
AgentWidgetRenderHistoryHeaderContext,
|
|
167
|
+
AgentWidgetRenderHistoryConversationContext,
|
|
168
|
+
AgentWidgetRenderHistoryStateContext,
|
|
169
|
+
AgentWidgetRenderHistoryOpenErrorContext,
|
|
170
|
+
ClientVisitorGrant,
|
|
145
171
|
AgentWidgetSuggestion,
|
|
146
172
|
AgentWidgetSuggestionChipsConfig,
|
|
147
173
|
AgentWidgetResolvedSuggestion,
|
|
@@ -159,6 +185,26 @@ export type {
|
|
|
159
185
|
AgentWidgetWelcomeDismiss
|
|
160
186
|
} from "./types";
|
|
161
187
|
|
|
188
|
+
// Custom conversation-history backends (`features.history.provider`). The
|
|
189
|
+
// provider registry beside this module stays private.
|
|
190
|
+
export type {
|
|
191
|
+
HistoryProvider,
|
|
192
|
+
HistoryOperationContext,
|
|
193
|
+
PreparedHistoryActivation,
|
|
194
|
+
HistoryListOptions,
|
|
195
|
+
HistoryListResult,
|
|
196
|
+
HistoryPageOptions,
|
|
197
|
+
HistoryPageResult,
|
|
198
|
+
HistoryDeleteAllOptions,
|
|
199
|
+
HistoryProviderErrorCode,
|
|
200
|
+
} from "./internal/history-provider";
|
|
201
|
+
|
|
202
|
+
// Value export: custom providers construct and `instanceof` this.
|
|
203
|
+
export {
|
|
204
|
+
HistoryProviderError,
|
|
205
|
+
isHistoryProviderError,
|
|
206
|
+
} from "./internal/history-provider";
|
|
207
|
+
|
|
162
208
|
export {
|
|
163
209
|
applyFeaturePreferences,
|
|
164
210
|
parseWidgetPreferenceSlice,
|
|
@@ -347,7 +393,9 @@ export type {
|
|
|
347
393
|
AgentWidgetTransformSuggestionsContext,
|
|
348
394
|
AgentWidgetRenderSuggestionContext,
|
|
349
395
|
AgentWidgetRenderWelcomeContext,
|
|
350
|
-
AgentWidgetSuggestionSelectContext
|
|
396
|
+
AgentWidgetSuggestionSelectContext,
|
|
397
|
+
AgentWidgetRailSection,
|
|
398
|
+
AgentWidgetRailSectionContext
|
|
351
399
|
} from "./plugins/types";
|
|
352
400
|
// Welcome resolver: `renderWelcome` receives the resolved shape.
|
|
353
401
|
export { resolveWelcomeConfig, isWelcomeVisible } from "./welcome";
|
|
@@ -398,7 +446,7 @@ export { renderLucideIcon } from "./utils/icons";
|
|
|
398
446
|
export type { IconName } from "./utils/icons";
|
|
399
447
|
|
|
400
448
|
// Button utility exports
|
|
401
|
-
export { createIconButton, createLabelButton, createToggleGroup, createComboButton } from "./utils/buttons";
|
|
449
|
+
export { createIconButton, createLabelButton, createToggleGroup, createComboButton, createSplitButton } from "./utils/buttons";
|
|
402
450
|
export type {
|
|
403
451
|
CreateIconButtonOptions,
|
|
404
452
|
CreateLabelButtonOptions,
|
package/src/index-global.ts
CHANGED
|
@@ -188,3 +188,58 @@ setContextMentionsInlineLoader(() => {
|
|
|
188
188
|
}
|
|
189
189
|
return import(/* @vite-ignore */ chunkUrl);
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Deferred history-view loading.
|
|
194
|
+
//
|
|
195
|
+
// This bundle is built with `@runtypelabs/persona/history-view` external (see
|
|
196
|
+
// `tsup.global.config.ts`): the Messages navigation surface is kept out of the
|
|
197
|
+
// CDN payload and fetched only when the visitor first opens history. Same
|
|
198
|
+
// sibling-URL scheme as the chunks above.
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
import { setHistoryViewLoader } from "./history-view-loader";
|
|
202
|
+
|
|
203
|
+
setHistoryViewLoader(() => {
|
|
204
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
205
|
+
/index\.global\.js($|\?)/,
|
|
206
|
+
"history-view.js$1",
|
|
207
|
+
);
|
|
208
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
209
|
+
return Promise.reject(
|
|
210
|
+
new Error(
|
|
211
|
+
"Could not derive the history-view.js URL from the widget script URL " +
|
|
212
|
+
`(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
|
|
213
|
+
"rename index.global.js should host history-view.js alongside it.",
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Deferred event-stream-view loading.
|
|
222
|
+
//
|
|
223
|
+
// Same scheme as history-view: `@runtypelabs/persona/event-stream-view` is
|
|
224
|
+
// external here (see `tsup.global.config.ts`), and the observability panel is
|
|
225
|
+
// fetched from a sibling URL only when the visitor first opens it.
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
import { setEventStreamViewLoader } from "./event-stream-view-loader";
|
|
229
|
+
|
|
230
|
+
setEventStreamViewLoader(() => {
|
|
231
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
232
|
+
/index\.global\.js($|\?)/,
|
|
233
|
+
"event-stream-view.js$1",
|
|
234
|
+
);
|
|
235
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
236
|
+
return Promise.reject(
|
|
237
|
+
new Error(
|
|
238
|
+
"Could not derive the event-stream-view.js URL from the widget script URL " +
|
|
239
|
+
`(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
|
|
240
|
+
"rename index.global.js should host event-stream-view.js alongside it.",
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
245
|
+
});
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createDemoHistoryProvider,
|
|
4
|
+
type DemoHistoryConversationSeed,
|
|
5
|
+
type DemoHistoryProvider,
|
|
6
|
+
} from "./demo-history-provider";
|
|
7
|
+
import {
|
|
8
|
+
HistoryProviderError,
|
|
9
|
+
isHistoryProviderError,
|
|
10
|
+
type HistoryOperationContext,
|
|
11
|
+
} from "./history-provider";
|
|
12
|
+
import { isHistoryDisplayUnavailable } from "../utils/history-messages";
|
|
13
|
+
|
|
14
|
+
const FIXED_NOW = Date.parse("2026-08-10T12:00:00.000Z");
|
|
15
|
+
const now = () => FIXED_NOW;
|
|
16
|
+
const browser: HistoryOperationContext = { scope: "browser" };
|
|
17
|
+
const verified: HistoryOperationContext = { scope: "verified-user" };
|
|
18
|
+
|
|
19
|
+
function provider(
|
|
20
|
+
options: Parameters<typeof createDemoHistoryProvider>[0] = {}
|
|
21
|
+
): DemoHistoryProvider {
|
|
22
|
+
return createDemoHistoryProvider({ now, ...options });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function seedWith(messageCount: number): DemoHistoryConversationSeed[] {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
id: "c1",
|
|
29
|
+
title: "Paging",
|
|
30
|
+
targetId: "t1",
|
|
31
|
+
messages: Array.from({ length: messageCount }, (_, index) => ({
|
|
32
|
+
role: index % 2 === 0 ? ("user" as const) : ("assistant" as const),
|
|
33
|
+
content: `m${index + 1}`,
|
|
34
|
+
createdAt: new Date(FIXED_NOW - (messageCount - index) * 1000).toISOString(),
|
|
35
|
+
})),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.useRealTimers();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("capabilities", () => {
|
|
45
|
+
it("advertises browser scope only and omits the optional capabilities", () => {
|
|
46
|
+
const demo = provider();
|
|
47
|
+
expect(demo.capabilities.scopes).toEqual(["browser"]);
|
|
48
|
+
expect(demo.resetDevice).toBeUndefined();
|
|
49
|
+
expect(demo.subscribeAvailability).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects an unsupported scope locally", async () => {
|
|
53
|
+
const demo = provider();
|
|
54
|
+
await expect(demo.list({ context: verified })).rejects.toMatchObject({
|
|
55
|
+
code: "unsupported_scope",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("transcript paging", () => {
|
|
61
|
+
it("returns 25 / 25 / rest newest-page-first with oldest-first pages", async () => {
|
|
62
|
+
const demo = provider({ conversations: seedWith(52) });
|
|
63
|
+
|
|
64
|
+
const first = await demo.getPage("c1", { context: browser });
|
|
65
|
+
expect(first.messages).toHaveLength(25);
|
|
66
|
+
expect(first.messages[0].content).toBe("m28");
|
|
67
|
+
expect(first.messages[24].content).toBe("m52");
|
|
68
|
+
expect(first.nextCursor).not.toBeNull();
|
|
69
|
+
expect(first.summary.messageCount).toBe(52);
|
|
70
|
+
expect(first.conversationRevision).toBe(demo.getConversationRevision("c1"));
|
|
71
|
+
|
|
72
|
+
const second = await demo.getPage("c1", {
|
|
73
|
+
context: browser,
|
|
74
|
+
cursor: first.nextCursor as string,
|
|
75
|
+
});
|
|
76
|
+
expect(second.messages).toHaveLength(25);
|
|
77
|
+
expect(second.messages[0].content).toBe("m3");
|
|
78
|
+
expect(second.messages[24].content).toBe("m27");
|
|
79
|
+
expect(second.nextCursor).not.toBeNull();
|
|
80
|
+
|
|
81
|
+
const third = await demo.getPage("c1", {
|
|
82
|
+
context: browser,
|
|
83
|
+
cursor: second.nextCursor as string,
|
|
84
|
+
});
|
|
85
|
+
expect(third.messages.map((m) => m.content)).toEqual(["m1", "m2"]);
|
|
86
|
+
expect(third.nextCursor).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps a single page when the transcript fits and is stable across calls", async () => {
|
|
90
|
+
const demo = provider({ conversations: seedWith(7) });
|
|
91
|
+
const first = await demo.getPage("c1", { context: browser });
|
|
92
|
+
const again = await demo.getPage("c1", { context: browser });
|
|
93
|
+
expect(first.nextCursor).toBeNull();
|
|
94
|
+
expect(first.messages.map((m) => m.id)).toEqual(again.messages.map((m) => m.id));
|
|
95
|
+
expect(first.messages.map((m) => m.content)).toEqual([
|
|
96
|
+
"m1",
|
|
97
|
+
"m2",
|
|
98
|
+
"m3",
|
|
99
|
+
"m4",
|
|
100
|
+
"m5",
|
|
101
|
+
"m6",
|
|
102
|
+
"m7",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects an unknown conversation with not_found", async () => {
|
|
107
|
+
const demo = provider();
|
|
108
|
+
const error = await demo
|
|
109
|
+
.getPage("nope", { context: browser })
|
|
110
|
+
.catch((err: unknown) => err);
|
|
111
|
+
expect(isHistoryProviderError(error)).toBe(true);
|
|
112
|
+
expect((error as HistoryProviderError).code).toBe("not_found");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns already-mapped messages including parts and the unavailable marker", async () => {
|
|
116
|
+
const demo = provider();
|
|
117
|
+
const refund = await demo.getPage("demo-conv-damaged-mug", { context: browser });
|
|
118
|
+
const oldest = await demo.getPage("demo-conv-damaged-mug", {
|
|
119
|
+
context: browser,
|
|
120
|
+
cursor: refund.nextCursor as string,
|
|
121
|
+
});
|
|
122
|
+
expect(oldest.messages[0].contentParts?.[1]?.type).toBe("image");
|
|
123
|
+
expect(oldest.messages[1].contentParts?.[1]?.type).toBe("file");
|
|
124
|
+
|
|
125
|
+
const bulk = await demo.getPage("demo-conv-bulk-order", { context: browser });
|
|
126
|
+
const withheld = bulk.messages.filter(isHistoryDisplayUnavailable);
|
|
127
|
+
expect(withheld).toHaveLength(1);
|
|
128
|
+
expect(withheld[0].content).toBe("");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("list paging", () => {
|
|
133
|
+
it("chains cursors in updatedAt-descending order", async () => {
|
|
134
|
+
const demo = provider();
|
|
135
|
+
const all = demo.getConversationIds();
|
|
136
|
+
|
|
137
|
+
const first = await demo.list({ context: browser, limit: 2 });
|
|
138
|
+
expect(first.items.map((item) => item.id)).toEqual(all.slice(0, 2));
|
|
139
|
+
expect(first.nextCursor).not.toBeNull();
|
|
140
|
+
|
|
141
|
+
const second = await demo.list({
|
|
142
|
+
context: browser,
|
|
143
|
+
limit: 2,
|
|
144
|
+
cursor: first.nextCursor as string,
|
|
145
|
+
});
|
|
146
|
+
expect(second.items.map((item) => item.id)).toEqual(all.slice(2, 4));
|
|
147
|
+
|
|
148
|
+
const third = await demo.list({
|
|
149
|
+
context: browser,
|
|
150
|
+
limit: 2,
|
|
151
|
+
cursor: second.nextCursor as string,
|
|
152
|
+
});
|
|
153
|
+
expect(third.items.map((item) => item.id)).toEqual(all.slice(4));
|
|
154
|
+
expect(third.nextCursor).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("filters by targetId and carries previews plus message counts", async () => {
|
|
158
|
+
const demo = provider();
|
|
159
|
+
const filtered = await demo.list({ context: browser, targetId: "demo-support-agent" });
|
|
160
|
+
expect(filtered.items.map((item) => item.id)).toEqual(["demo-conv-gift-wrap"]);
|
|
161
|
+
expect(filtered.items[0].messageCount).toBe(2);
|
|
162
|
+
expect(filtered.items[0].preview).toBeTruthy();
|
|
163
|
+
expect(filtered.items[0].preview?.length).toBeLessThanOrEqual(140);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("delete semantics", () => {
|
|
168
|
+
it("delete removes exactly one conversation", async () => {
|
|
169
|
+
const demo = provider();
|
|
170
|
+
const before = demo.getConversationIds();
|
|
171
|
+
await demo.delete("demo-conv-bulk-order", { context: browser });
|
|
172
|
+
expect(demo.getConversationIds()).toEqual(
|
|
173
|
+
before.filter((id) => id !== "demo-conv-bulk-order")
|
|
174
|
+
);
|
|
175
|
+
await expect(
|
|
176
|
+
demo.delete("demo-conv-bulk-order", { context: browser })
|
|
177
|
+
).rejects.toMatchObject({ code: "not_found" });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("deleteAll respects the targetId filter", async () => {
|
|
181
|
+
const demo = provider();
|
|
182
|
+
const result = await demo.deleteAll({ context: browser, targetId: "demo-flow" });
|
|
183
|
+
expect(result.deleted).toBe(4);
|
|
184
|
+
expect(demo.getConversationIds()).toEqual(["demo-conv-gift-wrap"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("deleteAll without a targetId clears the whole scope", async () => {
|
|
188
|
+
const demo = provider();
|
|
189
|
+
const result = await demo.deleteAll({ context: browser });
|
|
190
|
+
expect(result.deleted).toBe(5);
|
|
191
|
+
expect(demo.getConversationIds()).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("deleting the active conversation clears the active id", async () => {
|
|
195
|
+
const demo = provider();
|
|
196
|
+
const prepared = await demo.prepareOpen("demo-conv-order-status", { context: browser });
|
|
197
|
+
await prepared.commit();
|
|
198
|
+
await demo.delete("demo-conv-order-status", { context: browser });
|
|
199
|
+
expect(demo.getActiveConversationId()).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("prepared activations", () => {
|
|
204
|
+
it("prepareOpen changes nothing until commit", async () => {
|
|
205
|
+
const demo = provider();
|
|
206
|
+
const prepared = await demo.prepareOpen("demo-conv-bulk-order", { context: browser });
|
|
207
|
+
expect(prepared.conversationId).toBe("demo-conv-bulk-order");
|
|
208
|
+
expect(prepared.conversationRevision).toBe(
|
|
209
|
+
demo.getConversationRevision("demo-conv-bulk-order")
|
|
210
|
+
);
|
|
211
|
+
expect(demo.getActiveConversationId()).toBeNull();
|
|
212
|
+
|
|
213
|
+
await prepared.commit();
|
|
214
|
+
expect(demo.getActiveConversationId()).toBe("demo-conv-bulk-order");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("discard is inert and wins over a later commit", async () => {
|
|
218
|
+
const demo = provider();
|
|
219
|
+
const first = await demo.prepareOpen("demo-conv-bulk-order", { context: browser });
|
|
220
|
+
const second = await demo.prepareOpen("demo-conv-gift-wrap", { context: browser });
|
|
221
|
+
|
|
222
|
+
first.discard();
|
|
223
|
+
first.discard();
|
|
224
|
+
expect(demo.getActiveConversationId()).toBeNull();
|
|
225
|
+
|
|
226
|
+
await first.commit();
|
|
227
|
+
expect(demo.getActiveConversationId()).toBeNull();
|
|
228
|
+
|
|
229
|
+
await second.commit();
|
|
230
|
+
await second.commit();
|
|
231
|
+
expect(demo.getActiveConversationId()).toBe("demo-conv-gift-wrap");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("prepareStartNew creates the conversation only on commit", async () => {
|
|
235
|
+
const demo = provider();
|
|
236
|
+
const discarded = await demo.prepareStartNew({ context: browser });
|
|
237
|
+
discarded.discard();
|
|
238
|
+
await discarded.commit();
|
|
239
|
+
expect(demo.getConversationIds()).toHaveLength(5);
|
|
240
|
+
expect(demo.getActiveConversationId()).toBeNull();
|
|
241
|
+
|
|
242
|
+
const prepared = await demo.prepareStartNew({ context: browser });
|
|
243
|
+
await prepared.commit();
|
|
244
|
+
expect(demo.getConversationIds()).toHaveLength(6);
|
|
245
|
+
expect(demo.getActiveConversationId()).toBe(prepared.conversationId);
|
|
246
|
+
// Empty conversations are never listed, matching the API.
|
|
247
|
+
const listed = await demo.list({ context: browser });
|
|
248
|
+
expect(listed.items.map((item) => item.id)).not.toContain(prepared.conversationId);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("revisions", () => {
|
|
253
|
+
it("advances on every mutation and refreshes the summary", async () => {
|
|
254
|
+
const demo = provider();
|
|
255
|
+
const before = demo.getConversationRevision("demo-conv-order-status");
|
|
256
|
+
demo.appendMessage("demo-conv-order-status", {
|
|
257
|
+
role: "assistant",
|
|
258
|
+
content: "One more thing about your delivery window.",
|
|
259
|
+
});
|
|
260
|
+
const after = demo.getConversationRevision("demo-conv-order-status");
|
|
261
|
+
expect(after).not.toBe(before);
|
|
262
|
+
|
|
263
|
+
demo.appendMessage("demo-conv-order-status", {
|
|
264
|
+
role: "user",
|
|
265
|
+
content: "Understood, thanks.",
|
|
266
|
+
});
|
|
267
|
+
expect(demo.getConversationRevision("demo-conv-order-status")).not.toBe(after);
|
|
268
|
+
|
|
269
|
+
const page = await demo.getPage("demo-conv-order-status", { context: browser });
|
|
270
|
+
expect(page.summary.messageCount).toBe(6);
|
|
271
|
+
expect(page.summary.preview).toBe("Understood, thanks.");
|
|
272
|
+
expect(page.conversationRevision).toBe(
|
|
273
|
+
demo.getConversationRevision("demo-conv-order-status")
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("mints distinct revisions per conversation", () => {
|
|
278
|
+
const demo = provider();
|
|
279
|
+
const revisions = demo
|
|
280
|
+
.getConversationIds()
|
|
281
|
+
.map((id) => demo.getConversationRevision(id));
|
|
282
|
+
expect(new Set(revisions).size).toBe(revisions.length);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("identity status injection", () => {
|
|
287
|
+
it("defaults to browser_only / no_identity_provider", () => {
|
|
288
|
+
expect(provider().getIdentityStatus()).toEqual({
|
|
289
|
+
state: "browser_only",
|
|
290
|
+
reason: "no_identity_provider",
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("notifies subscribers on change and dedupes identical states", () => {
|
|
295
|
+
const demo = provider();
|
|
296
|
+
const seen: unknown[] = [];
|
|
297
|
+
const unsubscribe = demo.subscribeIdentityStatus((status) => seen.push(status));
|
|
298
|
+
|
|
299
|
+
demo.setIdentityStatus({ state: "verifying" });
|
|
300
|
+
demo.setIdentityStatus({ state: "verifying" });
|
|
301
|
+
demo.setIdentityStatus({ state: "verified" });
|
|
302
|
+
demo.setIdentityStatus({
|
|
303
|
+
state: "authentication_required",
|
|
304
|
+
reason: "invalid_identity_proof",
|
|
305
|
+
});
|
|
306
|
+
demo.setIdentityStatus({
|
|
307
|
+
state: "authentication_required",
|
|
308
|
+
reason: "proof_unavailable_after_binding",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(seen).toEqual([
|
|
312
|
+
{ state: "verifying" },
|
|
313
|
+
{ state: "verified" },
|
|
314
|
+
{ state: "authentication_required", reason: "invalid_identity_proof" },
|
|
315
|
+
{ state: "authentication_required", reason: "proof_unavailable_after_binding" },
|
|
316
|
+
]);
|
|
317
|
+
expect(demo.getIdentityStatus()).toEqual({
|
|
318
|
+
state: "authentication_required",
|
|
319
|
+
reason: "proof_unavailable_after_binding",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
unsubscribe();
|
|
323
|
+
demo.setIdentityStatus({ state: "identity_provider_failed" });
|
|
324
|
+
expect(seen).toHaveLength(4);
|
|
325
|
+
expect(demo.getIdentityStatus()).toEqual({ state: "identity_provider_failed" });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("accepts an injected initial status", () => {
|
|
329
|
+
const demo = provider({ identityStatus: { state: "verified" } });
|
|
330
|
+
expect(demo.getIdentityStatus()).toEqual({ state: "verified" });
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("failure injection", () => {
|
|
335
|
+
it("failNext fails exactly once per queued entry", async () => {
|
|
336
|
+
const demo = provider();
|
|
337
|
+
demo.failNext("list", { code: "rate_limited", retryAfterSeconds: 30 });
|
|
338
|
+
const error = await demo.list({ context: browser }).catch((err: unknown) => err);
|
|
339
|
+
expect((error as HistoryProviderError).code).toBe("rate_limited");
|
|
340
|
+
expect((error as HistoryProviderError).retryAfterSeconds).toBe(30);
|
|
341
|
+
|
|
342
|
+
const recovered = await demo.list({ context: browser });
|
|
343
|
+
expect(recovered.items).toHaveLength(5);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("persistent failures apply until cleared, per operation", async () => {
|
|
347
|
+
const demo = provider({ failures: { getPage: { code: "unavailable" } } });
|
|
348
|
+
await expect(
|
|
349
|
+
demo.getPage("demo-conv-order-status", { context: browser })
|
|
350
|
+
).rejects.toMatchObject({ code: "unavailable" });
|
|
351
|
+
await expect(
|
|
352
|
+
demo.getPage("demo-conv-order-status", { context: browser })
|
|
353
|
+
).rejects.toMatchObject({ code: "unavailable" });
|
|
354
|
+
await expect(demo.list({ context: browser })).resolves.toBeTruthy();
|
|
355
|
+
|
|
356
|
+
demo.setFailure("deleteAll", { code: "authentication_required" });
|
|
357
|
+
await expect(demo.deleteAll({ context: browser })).rejects.toMatchObject({
|
|
358
|
+
code: "authentication_required",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
demo.clearFailures();
|
|
362
|
+
await expect(
|
|
363
|
+
demo.getPage("demo-conv-order-status", { context: browser })
|
|
364
|
+
).resolves.toBeTruthy();
|
|
365
|
+
await expect(demo.deleteAll({ context: browser })).resolves.toEqual({ deleted: 5 });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("a failed mutation leaves the store untouched", async () => {
|
|
369
|
+
const demo = provider();
|
|
370
|
+
demo.failNext("delete", { code: "authentication_failed" });
|
|
371
|
+
await expect(
|
|
372
|
+
demo.delete("demo-conv-order-status", { context: browser })
|
|
373
|
+
).rejects.toMatchObject({ code: "authentication_failed" });
|
|
374
|
+
expect(demo.getConversationIds()).toHaveLength(5);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("latency injection", () => {
|
|
379
|
+
it("delays operations by the configured latency", async () => {
|
|
380
|
+
vi.useFakeTimers();
|
|
381
|
+
const demo = provider({ latencyMs: 250 });
|
|
382
|
+
let settled = false;
|
|
383
|
+
const pending = demo.list({ context: browser }).then((result) => {
|
|
384
|
+
settled = true;
|
|
385
|
+
return result;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
389
|
+
expect(settled).toBe(false);
|
|
390
|
+
|
|
391
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
392
|
+
await pending;
|
|
393
|
+
expect(settled).toBe(true);
|
|
394
|
+
|
|
395
|
+
demo.setLatency(0);
|
|
396
|
+
settled = false;
|
|
397
|
+
await demo.list({ context: browser }).then(() => {
|
|
398
|
+
settled = true;
|
|
399
|
+
});
|
|
400
|
+
expect(settled).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
});
|