@jskit-ai/assistant-runtime 0.1.193 → 0.1.194

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.
Files changed (50) hide show
  1. package/fixtures/canonical-conversation/App.vue +188 -0
  2. package/fixtures/canonical-conversation/DraftConversation.vue +32 -0
  3. package/fixtures/canonical-conversation/index.html +1 -0
  4. package/fixtures/canonical-conversation/main.js +61 -0
  5. package/fixtures/canonical-conversation/socket.js +50 -0
  6. package/fixtures/canonical-conversation/vite.config.mjs +178 -0
  7. package/fixtures/conversation/vite.config.mjs +12 -2
  8. package/package.json +23 -10
  9. package/src/client/components/AssistantConversationClientElement.vue +57 -0
  10. package/src/client/composables/useAssistantConversation.js +874 -0
  11. package/src/client/index.js +2 -0
  12. package/src/client/support/assistantRuntimeState.js +1 -1
  13. package/src/client/support/subscribeAssistantConversation.js +205 -0
  14. package/src/server/AssistantProvider.js +82 -106
  15. package/src/server/actionIds.js +10 -0
  16. package/src/server/actions.js +287 -79
  17. package/src/server/createAssistantRuntime.js +138 -0
  18. package/src/server/createAssistantSettingsActions.js +89 -0
  19. package/src/server/index.js +6 -0
  20. package/src/server/inputSchemas.js +90 -0
  21. package/src/server/registerConversationSubscriptions.js +163 -0
  22. package/src/server/registerFastifyConversations.js +76 -0
  23. package/src/server/registerRoutes.js +220 -198
  24. package/src/server/registerSettingsRoutes.js +113 -0
  25. package/src/server/repositories/conversationsRepository.js +12 -5
  26. package/src/server/repositories/messagesRepository.js +12 -0
  27. package/src/server/services/chatService.js +88 -694
  28. package/src/server/services/transcriptService.js +56 -52
  29. package/src/server/sqlConversationStorage.js +192 -0
  30. package/src/server/support/assistantRouteContext.js +91 -0
  31. package/src/server/support/createSurfaceAwareToolCatalog.js +6 -2
  32. package/src/shared/assistantSurfaces.js +14 -0
  33. package/src/shared/conversationRealtime.js +9 -0
  34. package/src/shared/index.js +1 -0
  35. package/test/actionDefinitions.test.js +44 -0
  36. package/test/aiConnections.test.js +1 -1
  37. package/test/assistantConversationBinding.browser.test.js +1868 -0
  38. package/test/assistantConversationOperations.test.js +459 -0
  39. package/test/assistantConversationSubscriptions.test.js +692 -0
  40. package/test/assistantIntegrationBoundary.test.js +103 -0
  41. package/test/assistantRuntimeComposition.test.js +68 -0
  42. package/test/assistantSurfacePattern.test.js +1 -1
  43. package/test/assistantSurfaces.test.js +23 -0
  44. package/test/chatServiceLifecycle.test.js +12 -1
  45. package/test/featureRuntime.test.js +238 -0
  46. package/test/registerFastifyConversations.test.js +169 -0
  47. package/test/sqlConversationStorage.test.js +205 -0
  48. package/test/support/memoryAssistantRows.js +51 -0
  49. package/test/turnRequests.test.js +11 -2
  50. package/src/shared/assistantResponseText.js +0 -29
@@ -0,0 +1,1868 @@
1
+ import assert from "node:assert/strict";
2
+ import { fileURLToPath } from "node:url";
3
+ import test from "node:test";
4
+ import { chromium, expect } from "@playwright/test";
5
+ import { createChromiumLaunchOptions, startViteFixture, stopProcess } from "../../../tooling/testUtils/browserFixture.mjs";
6
+
7
+ const options = { skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1", timeout: 120_000 };
8
+
9
+ async function fixture(t, width = 1280, { acceptedDraft = false, draftWhileLoading = false, unresolved = false, applicationDefaults = false } = {}) {
10
+ const vite = await startViteFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/canonical-conversation/", import.meta.url)) });
11
+ const browser = await chromium.launch(createChromiumLaunchOptions());
12
+ const page = await browser.newPage({ viewport: { width, height: 900 } });
13
+ const errors = [];
14
+ const diagnostics = [];
15
+ page.on("pageerror", error => errors.push(error.message));
16
+ page.on("console", message => { if (message.type() === "error") diagnostics.push(message.text()); });
17
+ page.on("requestfailed", request => diagnostics.push(`${request.url()}: ${request.failure()?.errorText}`));
18
+ t.after(async () => { await browser.close(); await stopProcess(vite); assert.deepEqual(errors, []); });
19
+ const fixtureOptions = new URLSearchParams();
20
+ if (acceptedDraft) fixtureOptions.set("acceptedDraft", "");
21
+ if (draftWhileLoading) fixtureOptions.set("draftWhileLoading", "");
22
+ if (unresolved) fixtureOptions.set("unresolved", "");
23
+ if (applicationDefaults) fixtureOptions.set("applicationDefaults", "");
24
+ await page.goto(`${vite.baseURL}/?${fixtureOptions}`);
25
+ const primary = page.locator("[data-primary]");
26
+ const input = primary.getByRole("textbox", { name: "Message AI assistant" });
27
+ try { await expect(input).toBeEnabled(); }
28
+ catch (failure) {
29
+ t.diagnostic(JSON.stringify({ errors, diagnostics, vite: vite.readOutput(), html: await page.locator("body").innerHTML() }));
30
+ throw failure;
31
+ }
32
+ const command = (name, body = {}) => page.request.post(`${vite.baseURL}/fixture/${name}`, { data: body });
33
+ const state = async () => (await page.request.get(`${vite.baseURL}/fixture/state`)).json();
34
+ const sent = async () => (await state()).requests.filter(request => request.suffix === "/messages");
35
+ const notify = id => page.evaluate(id => window.conversationFixture.socket.notify(id), id);
36
+ return { page, primary, input, command, state, sent, notify };
37
+ }
38
+
39
+ test("the canonical standard element keeps draft/focus/layout while applying the shared stream snapshot", options, async t => {
40
+ const f = await fixture(t, 390);
41
+ await expect(f.primary.getByRole("button", { name: "Set goal", exact: true })).toHaveCount(0);
42
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("set", { objective: "Unsupported" })), false);
43
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/goal").length, 0);
44
+ await f.input.fill("First request");
45
+ await f.input.dispatchEvent("keydown", { key: "Enter", isComposing: true });
46
+ assert.equal((await f.sent()).length, 0);
47
+ await f.input.press("Enter");
48
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
49
+ await expect(f.input).toHaveValue("");
50
+ await f.input.fill("Draft typed while streaming. Keep my selection.");
51
+ await f.input.evaluate(element => { element.focus(); element.setSelectionRange(6, 11); window.fixtureInput = element; });
52
+ const [request] = await f.sent();
53
+ assert.equal(request.surface, "home");
54
+ assert.deepEqual(Object.keys(request.input).sort(), ["messageId", "text"]);
55
+ const messageId = `${request.input.messageId}:answer`;
56
+ await f.page.evaluate(messageId => window.conversationFixture.socket.notify("chat:1", {
57
+ type: "message", messageId, role: "assistant", status: "inProgress", text: "Partial answer",
58
+ streaming: { revision: 1, messages: [{ messageId, role: "assistant", status: "inProgress", text: "Partial answer" }] }
59
+ }), messageId);
60
+ await expect(f.primary.getByText("Partial answer", { exact: true })).toBeVisible();
61
+ await expect(f.input).toBeFocused();
62
+ assert.deepEqual(await f.input.evaluate(element => [element === window.fixtureInput, element.selectionStart, element.selectionEnd]), [true, 6, 11]);
63
+ await f.command("finish", { id: "chat:1", text: "Partial answer completed." });
64
+ await f.notify("chat:1");
65
+ await expect(f.primary.getByText("Partial answer completed.", { exact: true })).toHaveCount(1);
66
+ await expect(f.primary.getByText("Partial answer", { exact: true })).toHaveCount(0);
67
+ await expect(f.input).toHaveValue("Draft typed while streaming. Keep my selection.");
68
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
69
+ for (const width of [390, 800, 1365]) {
70
+ await f.page.setViewportSize({ width, height: 900 });
71
+ await expect(f.input).toBeVisible();
72
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
73
+ }
74
+ assert.equal((await f.sent()).length, 1);
75
+ await f.command("error", { id: "chat:1", error: "Restore access to conversation storage before continuing." });
76
+ await f.notify("chat:1");
77
+ await expect(f.primary.getByText("Restore access to conversation storage before continuing.", { exact: true })).toBeVisible();
78
+ });
79
+
80
+ test("optional goal controls follow declared command semantics and budgets without client engine branches", options, async t => {
81
+ const f = await fixture(t);
82
+ await f.input.fill("Keep this draft while changing goals");
83
+ const requests = async () => (await f.state()).requests.filter(request => request.suffix === "/goal" && request.method === "POST");
84
+ for (const budgets of [true, false]) {
85
+ const commands = {
86
+ set: { delivery: "message", interruptsTurn: false }, resume: { delivery: "message", interruptsTurn: false },
87
+ pause: { delivery: "control", interruptsTurn: !budgets },
88
+ cancel: { delivery: budgets ? "control" : "message", interruptsTurn: !budgets }
89
+ };
90
+ await f.command("goals", { id: "chat:1", budgets, commands });
91
+ await f.notify("chat:1");
92
+ await f.primary.getByRole("button", { name: "Set goal", exact: true }).click();
93
+ const objective = budgets ? "Finish with a bounded budget" : "Finish without a budget";
94
+ await f.page.getByRole("textbox", { name: "Goal objective" }).fill(objective);
95
+ const budget = f.page.getByRole("spinbutton", { name: "Token budget (optional)" });
96
+ if (budgets) await budget.fill("1000");
97
+ else await expect(budget).toHaveCount(0);
98
+ const start = f.page.getByRole("button", { name: "Start goal", exact: true });
99
+ await expect(start).toBeEnabled();
100
+ await start.click();
101
+ await expect(f.primary.getByRole("button", { name: "Goal running", exact: true })).toBeVisible();
102
+ const pause = f.page.getByRole("button", { name: "Pause goal", exact: true });
103
+ await expect(pause).toBeEnabled();
104
+ await expect(f.page.getByText(budgets
105
+ ? "Pausing prevents further automatic turns. Use Stop to interrupt the current turn."
106
+ : "Pause stops the current turn and keeps the goal for later.", { exact: true })).toBeVisible();
107
+ await pause.click();
108
+ await expect(f.primary.getByRole("button", { name: "Goal paused", exact: true })).toBeVisible();
109
+ const resume = f.page.getByRole("button", { name: "Resume goal", exact: true });
110
+ await expect(resume).toBeEnabled();
111
+ await resume.click();
112
+ await expect(f.primary.getByRole("button", { name: "Goal running", exact: true })).toBeVisible();
113
+ const cancel = f.page.getByRole("button", { name: "Cancel goal", exact: true });
114
+ await expect(cancel).toBeEnabled();
115
+ await cancel.click();
116
+ await expect(f.primary.getByRole("button", { name: "Set goal", exact: true })).toBeVisible();
117
+ const commandsSent = (await requests()).slice(-4).map(request => request.input);
118
+ assert.deepEqual(commandsSent.map(command => command.action), ["set", "pause", "resume", "cancel"]);
119
+ assert.ok(commandsSent[0].messageId);
120
+ assert.ok(commandsSent[2].messageId);
121
+ assert.equal(Object.hasOwn(commandsSent[1], "messageId"), false);
122
+ assert.equal(Object.hasOwn(commandsSent[3], "messageId"), !budgets);
123
+ assert.equal(commandsSent[0].tokenBudget, budgets ? 1000 : undefined);
124
+ assert.ok(commandsSent.every(command => command.expectedSegmentId === commandsSent[0].expectedSegmentId));
125
+ assert.ok(commandsSent.slice(1).every(command => command.expectedGoalId === `goal-${commandsSent[0].messageId}`));
126
+ assert.ok(commandsSent.every(command => !Object.hasOwn(command, "engine") && !Object.hasOwn(command, "threadId")));
127
+ await f.input.click();
128
+ await expect(f.input).toHaveValue("Keep this draft while changing goals");
129
+ }
130
+ assert.equal((await f.sent()).length, 0, "Goal commands use the existing goal operation, not ordinary send");
131
+ });
132
+
133
+ test("the first native goal preserves absent identity and later controls capture its actual thread", options, async t => {
134
+ const f = await fixture(t);
135
+ await f.input.fill("Keep this ordinary draft");
136
+ await f.command("goals", { id: "chat:1", segmentId: null, budgets: false, commands: {
137
+ set: { delivery: "control", interruptsTurn: false }, pause: { delivery: "control", interruptsTurn: false }
138
+ } });
139
+ await f.notify("chat:1");
140
+ await f.primary.getByRole("button", { name: "Set goal", exact: true }).click();
141
+ await f.page.getByRole("textbox", { name: "Goal objective" }).fill("Start the first native goal");
142
+ const start = f.page.getByRole("button", { name: "Start goal", exact: true });
143
+ await expect(start).toBeEnabled();
144
+ await start.click();
145
+ await expect(f.primary.getByRole("button", { name: "Goal running", exact: true })).toBeVisible();
146
+ const pause = f.page.getByRole("button", { name: "Pause goal", exact: true });
147
+ await expect(pause).toBeEnabled();
148
+ await pause.click();
149
+ await expect(f.primary.getByRole("button", { name: "Goal paused", exact: true })).toBeVisible();
150
+ const state = await f.state();
151
+ const commands = state.requests.filter(request => request.suffix === "/goal" && request.method === "POST").map(request => request.input);
152
+ assert.deepEqual(commands.map(command => command.action), ["set", "pause"]);
153
+ assert.equal(commands[0].expectedSegmentId, null, "The binding must not invent a segment for an unstarted native conversation");
154
+ assert.equal(commands[0].expectedGoalId, null);
155
+ assert.equal(commands[1].expectedSegmentId, "first-native-thread-chat:1");
156
+ assert.equal(commands[1].expectedGoalId, state.states["chat:1"].goal.id);
157
+ assert.ok(commands.every(command => !Object.hasOwn(command, "messageId") && !Object.hasOwn(command, "threadId")));
158
+ assert.deepEqual(state.states["chat:1"].conversationLog, [], "Native controls do not fabricate authored transcript receipts");
159
+ assert.deepEqual(await f.sent(), []);
160
+ await f.input.click();
161
+ await expect(f.input).toHaveValue("Keep this ordinary draft");
162
+ });
163
+
164
+ test("a lost goal receipt uses the same inspection-only ledger and never starts a second goal on reconnect", options, async t => {
165
+ const f = await fixture(t);
166
+ await f.command("goals", { id: "chat:1", budgets: false, commands: {
167
+ set: { delivery: "message", interruptsTurn: false }, pause: { delivery: "control", interruptsTurn: true }
168
+ } });
169
+ await f.notify("chat:1");
170
+ await f.primary.getByRole("button", { name: "Set goal", exact: true }).click();
171
+ await f.page.getByRole("textbox", { name: "Goal objective" }).fill("Finish the uncertain goal");
172
+ await f.command("mode", { mode: "uncertain" });
173
+ const start = f.page.getByRole("button", { name: "Start goal", exact: true });
174
+ await expect(start).toBeEnabled();
175
+ await start.click();
176
+ await f.input.click();
177
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
178
+ await expect(check).toBeVisible();
179
+ await expect(f.primary.getByText("Finish the uncertain goal", { exact: true })).toHaveCount(1);
180
+ const requests = async () => (await f.state()).requests.filter(request => request.suffix === "/goal" && request.method === "POST");
181
+ const [original] = await requests();
182
+ assert.ok(original.input.messageId);
183
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
184
+ await expect(check).toBeVisible();
185
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("set", { objective: "Do not repeat" })), false);
186
+ assert.equal((await requests()).length, 1);
187
+ await f.command("confirm", { id: "chat:1" });
188
+ await check.click();
189
+ await expect(check).toHaveCount(0);
190
+ await expect(f.primary.getByRole("button", { name: "Goal running", exact: true })).toBeVisible();
191
+ await expect(f.primary.getByText("Finish the uncertain goal", { exact: true })).toHaveCount(1);
192
+ assert.equal((await requests()).length, 1);
193
+ await f.primary.getByRole("button", { name: "Goal running", exact: true }).click();
194
+ await expect(f.page.getByRole("button", { name: "Cancel goal", exact: true })).toHaveCount(0);
195
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("cancel")), false);
196
+ assert.equal((await requests()).length, 1, "An undeclared goal command is not exposed or dispatched");
197
+ });
198
+
199
+ test("a supplied goal view keeps its pinned target and refreshes through the retained binding without polling", options, async t => {
200
+ const f = await fixture(t);
201
+ const goal = { id: "retained-goal", objective: "The plain product objective", status: "paused",
202
+ timeUsedSeconds: 12, updatedAt: "2026-10-04T00:00:00.000Z" };
203
+ const view = { status: "available", goal, routing: { mode: "senior", selection: { engineId: "other-native" } },
204
+ target: { segmentId: "retained-goal-segment", capabilities: { goals: true, goalBudgets: false,
205
+ goalCommands: { resume: { delivery: "control", interruptsTurn: false }, pause: { delivery: "control", interruptsTurn: true } } } } };
206
+ let reads = 0;
207
+ let failedRead = false;
208
+ const commands = [];
209
+ await f.page.route("**/conversations/*/goal", async route => {
210
+ if (route.request().method() === "GET") {
211
+ reads++;
212
+ return route.fulfill({ status: failedRead ? 503 : 200, json: failedRead ? { error: "Goal status is unavailable." } : view });
213
+ }
214
+ commands.push(route.request().postDataJSON());
215
+ return route.fulfill({ json: { ok: false, error: "The displayed goal changed." } });
216
+ });
217
+ await f.page.evaluate(async () => {
218
+ const timeout = AbortSignal.timeout.bind(AbortSignal);
219
+ const interval = window.setInterval.bind(window);
220
+ window.fixtureGoalTimeouts = [];
221
+ window.fixtureGoalIntervals = [];
222
+ AbortSignal.timeout = ms => { window.fixtureGoalTimeouts.push(ms); return timeout(ms); };
223
+ window.setInterval = (callback, ms, ...args) => { window.fixtureGoalIntervals.push(ms); return interval(callback, ms, ...args); };
224
+ await window.conversationFixture.eventReaders([{ id: "goal-reader", goal: true }]);
225
+ });
226
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalView.value)).toEqual(view);
227
+ assert.equal((await f.state()).states["chat:1"].capabilities.goals, false,
228
+ "An explicit supplied read can follow a retained goal while the visible engine has no goals");
229
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending)).toBe(false);
230
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("resume")), false);
231
+ assert.deepEqual(commands, [{ action: "resume", expectedSegmentId: view.target.segmentId, expectedGoalId: goal.id }]);
232
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.error), "The displayed goal changed.");
233
+ const beforeEvent = reads;
234
+ view.goal = { ...goal, status: "active", objective: "Still the plain product objective" };
235
+ await f.page.evaluate(() => window.conversationFixture.socket.notify("chat:1", { type: "goal" }));
236
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalView.value.goal.objective)).toBe(view.goal.objective);
237
+ assert.ok(reads > beforeEvent);
238
+ const beforeReconnect = reads;
239
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
240
+ await expect.poll(() => reads).toBeGreaterThan(beforeReconnect);
241
+ const beforeFocus = reads;
242
+ await f.page.evaluate(() => window.dispatchEvent(new Event("focus")));
243
+ await expect.poll(() => reads).toBeGreaterThan(beforeFocus);
244
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending)).toBe(false);
245
+ failedRead = true;
246
+ await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
247
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalLoadError.value)).toBe("Goal status is unavailable.");
248
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().error.value), "",
249
+ "Passive goal failure does not enter conversation or app-wide request recovery");
250
+ await expect(f.input).toBeEnabled();
251
+ const deadlines = await f.page.evaluate(() => window.fixtureGoalTimeouts);
252
+ assert.ok(deadlines.length > 0);
253
+ assert.ok(deadlines.every(ms => ms === 30_000));
254
+ assert.equal((await f.page.evaluate(() => window.fixtureGoalIntervals)).includes(60_000), false);
255
+ assert.equal(commands.length, 1, "Native events, reconnect, focus and passive recovery never retry a command");
256
+ await f.page.evaluate(() => {
257
+ window.retiredGoalRuntime = window.conversationFixture.current();
258
+ window.conversationFixture.actor("");
259
+ });
260
+ await expect.poll(() => f.page.evaluate(() => window.retiredGoalRuntime.goalView.value)).toBe(null);
261
+ assert.equal(await f.page.evaluate(() => window.retiredGoalRuntime.current.value), false);
262
+ assert.equal(await f.page.evaluate(() => window.retiredGoalRuntime.changeGoal("resume")), false);
263
+ assert.equal(commands.length, 1, "Retired viewer controls cannot target the next viewer's goal");
264
+ });
265
+
266
+ test("a rejected message goal retry keeps its original UUID and exact read target", options, async t => {
267
+ const f = await fixture(t);
268
+ const commands = [];
269
+ const view = { status: "available", goal: null, target: { segmentId: null,
270
+ capabilities: { goals: true, goalBudgets: false, goalCommands: { set: { delivery: "message", interruptsTurn: false } } } } };
271
+ await f.page.route("**/conversations/*/goal", async route => {
272
+ if (route.request().method() === "GET") return route.fulfill({ json: view });
273
+ const input = route.request().postDataJSON();
274
+ commands.push(input);
275
+ if (commands.length === 1) return route.fulfill({ status: 409,
276
+ json: { code: "conversation_goal_changed", error: "Goal changed before admission." } });
277
+ return route.fulfill({ json: { status: "accepted", messageId: input.messageId, turnId: "original-goal-receipt" } });
278
+ });
279
+ await f.page.evaluate(() => window.conversationFixture.eventReaders([{ id: "goal-reader", goal: true }]));
280
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalView.value)).toEqual(view);
281
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalState.value?.pending)).toBe(false);
282
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("set", { objective: "Original objective" })), false);
283
+ assert.equal(commands.length, 1);
284
+ assert.ok(commands[0].messageId);
285
+ assert.equal(commands[0].expectedSegmentId, null);
286
+ assert.equal(commands[0].expectedGoalId, null);
287
+ view.target.segmentId = "newly-observed-target";
288
+ view.target.capabilities.goalCommands.set.delivery = "control";
289
+ view.goal = { id: "newly-observed-goal", status: "complete", objective: "Another objective" };
290
+ await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
291
+ const accepted = await f.page.evaluate(messageId => window.conversationFixture.current().send(
292
+ { message: "Changed retry must be ignored" }, { messageId }), commands[0].messageId);
293
+ assert.equal(accepted.status, "accepted");
294
+ assert.deepEqual(commands[1], commands[0], "Retry does not substitute the newer target, goal, objective or UUID");
295
+ assert.deepEqual(await f.sent(), [], "Goal delivery stays on the existing goal command and receipt ledger");
296
+ });
297
+
298
+ test("canonical attachment receipts clear only captured uploads before a held HTTP response", options, async t => {
299
+ const f = await fixture(t, 390);
300
+ await f.page.evaluate(() => window.conversationFixture.attachments(true));
301
+ const files = f.primary.locator("input[type=file]");
302
+ const attachments = () => f.page.evaluate(() => window.conversationFixture.attachmentState());
303
+ await files.setInputFiles({ name: "original.txt", mimeType: "text/plain", buffer: Buffer.from("Original file") });
304
+ await expect.poll(async () => (await attachments()).ready.length).toBe(1);
305
+ const original = (await attachments()).ready[0];
306
+ await f.command("mode", { mode: "hold" });
307
+ await f.primary.getByRole("button", { name: "Send message", exact: true }).click();
308
+ await expect.poll(async () => (await f.state()).held).toBe(1);
309
+ const [request] = await f.sent();
310
+ assert.deepEqual(Object.keys(request.input).sort(), ["attachmentIds", "messageId", "text"]);
311
+ assert.deepEqual(request.input.attachmentIds, [original.attachmentId]);
312
+ assert.equal(request.input.text, "", "Attachment-only delivery preserves the authored empty text");
313
+ await files.setInputFiles([
314
+ { name: "newer.txt", mimeType: "text/plain", buffer: Buffer.from("Newer file") },
315
+ { name: "held-later.txt", mimeType: "text/plain", buffer: Buffer.from("Still uploading") }
316
+ ]);
317
+ await expect.poll(async () => (await attachments()).queue.map(row => row.phase)).toEqual(["ready", "ready", "uploading"]);
318
+ await f.input.fill("Keep the newer draft and uploads.");
319
+ await f.command("accept", { holdResponse: true });
320
+ await f.notify("chat:1");
321
+ await expect.poll(async () => (await attachments()).acknowledged).toEqual([[original.attachmentId]]);
322
+ assert.deepEqual((await attachments()).queue.map(row => row.fileName), ["newer.txt", "held-later.txt"]);
323
+ assert.deepEqual((await attachments()).deleted, []);
324
+ assert.equal((await f.state()).held, 1);
325
+ await f.command("release-response");
326
+ await f.page.evaluate(() => window.conversationFixture.finishUploads());
327
+ await expect.poll(async () => (await attachments()).ready.map(file => file.fileName)).toEqual(["newer.txt", "held-later.txt"]);
328
+ assert.deepEqual((await attachments()).acknowledged, [[original.attachmentId]]);
329
+ await expect(f.input).toHaveValue("Keep the newer draft and uploads.");
330
+ assert.equal((await f.sent()).length, 1);
331
+ });
332
+
333
+ test("explicit delivery inspection acknowledges uncertain attachments without clearing newer uploads or resending", options, async t => {
334
+ const f = await fixture(t);
335
+ await f.page.evaluate(() => window.conversationFixture.attachments(true));
336
+ const files = f.primary.locator("input[type=file]");
337
+ const attachments = () => f.page.evaluate(() => window.conversationFixture.attachmentState());
338
+ await files.setInputFiles({ name: "uncertain.txt", mimeType: "text/plain", buffer: Buffer.from("Keep this receipt") });
339
+ await expect.poll(async () => (await attachments()).ready.length).toBe(1);
340
+ const original = (await attachments()).ready[0];
341
+ await f.command("mode", { mode: "uncertain" });
342
+ await f.input.fill("Inspect this file.");
343
+ await f.input.press("Enter");
344
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
345
+ await expect(check).toBeVisible();
346
+ assert.deepEqual((await attachments()).acknowledged, []);
347
+ await files.setInputFiles({ name: "newer.txt", mimeType: "text/plain", buffer: Buffer.from("Keep this newer file") });
348
+ await expect.poll(async () => (await attachments()).ready.length).toBe(2);
349
+ await check.click();
350
+ await expect(check).toBeVisible();
351
+ assert.deepEqual((await attachments()).acknowledged, [], "An unknown receipt does not accept the upload");
352
+ await f.command("confirm", { id: "chat:1" });
353
+ await check.click();
354
+ await expect(check).toHaveCount(0);
355
+ await expect.poll(async () => (await attachments()).acknowledged).toEqual([[original.attachmentId]]);
356
+ assert.deepEqual((await attachments()).ready.map(file => file.fileName), ["newer.txt"]);
357
+ assert.deepEqual((await attachments()).deleted, []);
358
+ await f.notify("chat:1");
359
+ assert.equal((await f.sent()).length, 1);
360
+ assert.deepEqual((await attachments()).acknowledged, [[original.attachmentId]]);
361
+ });
362
+
363
+ test("attachment capability and upload gates preserve drafts and retired actors cannot clear a new queue", options, async t => {
364
+ const f = await fixture(t);
365
+ await f.page.evaluate(() => window.conversationFixture.attachments(true));
366
+ const files = f.primary.locator("input[type=file]");
367
+ const attachments = () => f.page.evaluate(() => window.conversationFixture.attachmentState());
368
+ await files.setInputFiles({ name: "held-upload.txt", mimeType: "text/plain", buffer: Buffer.from("Wait for upload") });
369
+ await f.input.fill("Wait for this file.");
370
+ await expect(f.primary.getByRole("button", { name: "Send message", exact: true })).toBeDisabled();
371
+ await f.input.press("Enter");
372
+ assert.equal((await f.sent()).length, 0);
373
+ // Disabled submission preserves ordinary textarea editing, including Enter.
374
+ await expect(f.input).toHaveValue("Wait for this file.\n");
375
+ await f.command("attachments", { id: "chat:1", enabled: false });
376
+ await f.notify("chat:1");
377
+ await expect(f.primary.getByRole("button", { name: "Attach files", exact: true })).toHaveCount(0);
378
+ await f.page.evaluate(() => window.conversationFixture.finishUploads());
379
+ await expect.poll(async () => (await attachments()).ready.length).toBe(1);
380
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().send({
381
+ message: "", displayAttachments: window.conversationFixture.attachmentState().ready
382
+ })), false);
383
+ assert.equal((await f.sent()).length, 0);
384
+ await f.command("attachments", { id: "chat:1", enabled: true });
385
+ await f.notify("chat:1");
386
+ await f.command("mode", { mode: "hold" });
387
+ await f.input.press("Enter");
388
+ await expect.poll(async () => (await f.state()).held).toBe(1);
389
+ await f.page.evaluate(() => window.conversationFixture.actor("43"));
390
+ await expect(f.input).toBeEnabled();
391
+ await files.setInputFiles({ name: "next-actor.txt", mimeType: "text/plain", buffer: Buffer.from("Next actor file") });
392
+ await expect.poll(async () => (await attachments()).ready.map(file => file.fileName)).toEqual(["next-actor.txt"]);
393
+ await f.command("accept");
394
+ await f.notify("chat:1");
395
+ await expect.poll(async () => (await f.state()).held).toBe(0);
396
+ assert.deepEqual((await attachments()).acknowledged, []);
397
+ assert.deepEqual((await attachments()).ready.map(file => file.fileName), ["next-actor.txt"]);
398
+ assert.equal((await f.sent()).length, 1);
399
+ });
400
+
401
+ test("canonical optional suggestions use an independent host generator and edit the draft without sending", options, async t => {
402
+ const f = await fixture(t, 390);
403
+ const suggestion = prompt => f.primary.getByRole("button", { name: `Use suggestion: ${prompt}`, exact: true });
404
+ const suggestions = () => f.page.evaluate(() => window.conversationFixture.suggestionState());
405
+ await expect(f.primary.getByRole("button", { name: /^Use suggestion:/u })).toHaveCount(0);
406
+ await f.page.evaluate(() => window.conversationFixture.suggestions(true));
407
+ await expect(suggestion("Suggested by suggestions-small")).toBeVisible();
408
+ const [original] = (await suggestions()).requests;
409
+ assert.deepEqual(original.configuration, { integrationId: "suggestions", model: "suggestions-small" });
410
+ assert.equal(original.target.conversationId, "chat:1");
411
+ assert.equal(original.target.actorKey, "42");
412
+ await suggestion("Suggested by suggestions-small").hover();
413
+ await expect(f.input).toHaveAttribute("placeholder", "Suggested by suggestions-small");
414
+ await expect(f.input).toHaveValue("");
415
+ await f.input.fill("Keep this draft until I choose a suggestion.");
416
+ await expect(suggestion("Suggested by suggestions-small")).toBeVisible();
417
+ await suggestion("Suggested by suggestions-small").hover();
418
+ await expect(f.input).toHaveValue("Keep this draft until I choose a suggestion.");
419
+ assert.equal((await f.sent()).length, 0);
420
+ await f.page.evaluate(() => window.conversationFixture.suggestionModel("suggestions-large"));
421
+ await suggestion("Suggested by suggestions-large").click();
422
+ await expect(f.input).toHaveValue("Suggested by suggestions-large");
423
+ await expect(f.input).toBeFocused();
424
+ assert.equal((await f.sent()).length, 0, "Choosing a suggestion edits the draft; Send stays explicit");
425
+ assert.equal((await suggestions()).requests[0].configuration.model, "suggestions-small");
426
+ await f.input.press("Enter");
427
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
428
+ await expect(f.primary.getByRole("button", { name: /^Use suggestion:/u })).toHaveCount(0);
429
+ assert.equal((await f.sent())[0].input.text, "Suggested by suggestions-large");
430
+ await f.input.fill("Keep typing while chat works.");
431
+ await expect(f.input).toHaveValue("Keep typing while chat works.");
432
+ assert.equal((await f.sent()).length, 1);
433
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
434
+ });
435
+
436
+ test("canonical suggestion scope cancels stale targets and actors while a voice reader retains the draft", options, async t => {
437
+ const f = await fixture(t);
438
+ const suggestions = () => f.page.evaluate(() => window.conversationFixture.suggestionState());
439
+ const resolve = (index, prompt) => f.page.evaluate(({ index, prompt }) => window.conversationFixture.resolveSuggestion(index, prompt), { index, prompt });
440
+ await f.page.evaluate(() => window.conversationFixture.suggestions(true, "hold"));
441
+ await expect.poll(async () => (await suggestions()).requests.length).toBe(1);
442
+ await f.page.evaluate(() => window.conversationFixture.target("chat:2"));
443
+ await expect.poll(async () => (await suggestions()).requests.at(-1)?.target.conversationId).toBe("chat:2");
444
+ assert.equal((await suggestions()).requests[0].aborted, true);
445
+ await resolve(0, "Obsolete conversation suggestion");
446
+ assert.deepEqual((await suggestions()).items, []);
447
+ const previousActor = (await suggestions()).requests.length - 1;
448
+ await f.page.evaluate(() => window.conversationFixture.actor("43"));
449
+ await expect.poll(async () => (await suggestions()).requests.at(-1)?.target.actorKey).toBe("43");
450
+ assert.equal((await suggestions()).requests[previousActor].aborted, true);
451
+ await resolve(previousActor, "Obsolete actor suggestion");
452
+ assert.deepEqual((await suggestions()).items, []);
453
+ await resolve((await suggestions()).requests.length - 1, "Current actor suggestion");
454
+ await f.primary.getByRole("button", { name: "Use suggestion: Current actor suggestion", exact: true }).click();
455
+ await expect(f.input).toHaveValue("Current actor suggestion");
456
+ await expect.poll(async () => (await suggestions()).requests.at(-1)?.draft).toBe("Current actor suggestion");
457
+ const hiddenRequest = (await suggestions()).requests.length - 1;
458
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.show(false); });
459
+ await expect.poll(async () => (await suggestions()).requests[hiddenRequest].aborted).toBe(true);
460
+ await resolve(hiddenRequest, "Late hidden suggestion");
461
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, "Current actor suggestion");
462
+ assert.deepEqual((await suggestions()).items, []);
463
+ await f.page.evaluate(() => window.conversationFixture.show(true));
464
+ await expect(f.input).toHaveValue("Current actor suggestion");
465
+ await expect.poll(async () => (await suggestions()).requests.length).toBeGreaterThan(hiddenRequest + 1);
466
+ await resolve((await suggestions()).requests.length - 1, "Returned conversation suggestion");
467
+ await expect(f.primary.getByRole("button", { name: "Use suggestion: Returned conversation suggestion", exact: true })).toBeVisible();
468
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.socket.inspect().active), 1);
469
+ await f.command("deny", { denied: true });
470
+ await f.notify("chat:2");
471
+ await expect(f.input).toBeDisabled();
472
+ await expect(f.primary.getByRole("button", { name: /^Use suggestion:/u })).toHaveCount(0);
473
+ assert.deepEqual((await suggestions()).items, []);
474
+ assert.equal((await f.sent()).length, 0);
475
+ });
476
+
477
+ test("text, mirror and voice retain one target while five chats share the supplied socket and release only observers", options, async t => {
478
+ const f = await fixture(t);
479
+ await f.input.fill("Retained draft");
480
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.mirror(true); });
481
+ await expect(f.page.locator("[data-mirror]").getByRole("textbox", { name: "Message AI assistant" })).toHaveValue("Retained draft");
482
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.socket.inspect().active), 1);
483
+ await f.page.evaluate(() => window.conversationFixture.multiple(true));
484
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.socket.inspect().active)).toBe(5);
485
+ await f.page.evaluate(() => { window.conversationFixture.multiple(false); window.conversationFixture.mirror(false); window.conversationFixture.show(false); });
486
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.socket.inspect().active)).toBe(1);
487
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, "Retained draft");
488
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.sendVoice("Voice stays with its retained target."))).status, "accepted");
489
+ await expect.poll(async () => (await f.page.evaluate(() => window.conversationFixture.voiceState())).turns.length).toBe(1);
490
+ assert.equal((await f.sent())[0].id, "chat:1");
491
+ await f.page.evaluate(() => { window.conversationFixture.target("chat:2"); window.conversationFixture.show(true); });
492
+ await expect(f.input).toBeEnabled();
493
+ await expect(f.input).toHaveValue("");
494
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).id, "chat:1");
495
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.socket.inspect().active)).toBe(2);
496
+ await f.page.evaluate(() => { window.conversationFixture.show(false); window.conversationFixture.actor("43"); });
497
+ await expect.poll(async () => (await f.page.evaluate(() => window.conversationFixture.voiceState())).current).toBe(false);
498
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("A retired identity must not send.")), false);
499
+ const retired = await f.page.evaluate(() => window.conversationFixture.voiceState());
500
+ assert.deepEqual(retired.turns, []);
501
+ assert.equal(retired.draft, "");
502
+ await f.page.evaluate(() => window.conversationFixture.releaseVoice());
503
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.socket.inspect().active)).toBe(0);
504
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.socket.inspect().listeners), 0);
505
+ assert.equal((await f.sent()).length, 1);
506
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 0);
507
+ });
508
+
509
+ test("retained event hooks notify active mirrored readers in state order and leave no callbacks after release", options, async t => {
510
+ const f = await fixture(t);
511
+ const readers = value => f.page.evaluate(value => window.conversationFixture.eventReaders(value), value);
512
+ const events = () => f.page.evaluate(() => window.conversationFixture.readerEvents());
513
+ const socket = () => f.page.evaluate(() => window.conversationFixture.socket.inspect());
514
+ const notify = event => f.page.evaluate(event => window.conversationFixture.socket.notify("chat:1", event), event);
515
+ const initialCalls = (await socket()).calls.length;
516
+ const initialRequests = (await f.state()).requests.length;
517
+ await readers([{ id: "first" }, { id: "second" }]);
518
+ assert.equal((await socket()).active, 1);
519
+ assert.equal((await socket()).calls.length, initialCalls, "Mirrored hooks reuse the existing subscription");
520
+ await notify({ type: "application", marker: "both", failure: "throw" });
521
+ assert.deepEqual((await events()).map(({ reader, marker }) => [reader, marker]), [["first", "both"], ["second", "both"]]);
522
+
523
+ await notify({ type: "message", marker: "stream", failure: "reject", messageId: "streamed-answer", role: "assistant",
524
+ status: "inProgress", text: "Applied before notification", streaming: { revision: 1,
525
+ messages: [{ messageId: "streamed-answer", role: "assistant", status: "inProgress", text: "Applied before notification" }] } });
526
+ assert.deepEqual((await events()).slice(-2), ["first", "second"].map(reader => ({
527
+ reader, type: "message", marker: "stream", revision: 1, text: "Applied before notification"
528
+ })), "A failed hook cannot interrupt the next reader or precede canonical stream application");
529
+ await expect(f.primary.getByText("Applied before notification", { exact: true })).toBeVisible();
530
+ assert.equal((await f.state()).requests.length, initialRequests, "Presentation notifications do not add canonical reads");
531
+
532
+ await readers([{ id: "first", active: false }, { id: "second" }]);
533
+ await notify({ type: "application", marker: "active-only" });
534
+ assert.deepEqual((await events()).slice(4).map(({ reader, marker }) => [reader, marker]), [["second", "active-only"]]);
535
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.show(false); });
536
+ await expect(f.primary).toHaveCount(0);
537
+ await readers([{ id: "first", active: false }]);
538
+ await notify({ type: "application", marker: "released-second" });
539
+ assert.equal((await events()).length, 5, "Inactive and released readers receive no events while voice retains the target");
540
+ assert.equal((await socket()).active, 1);
541
+
542
+ await readers([{ id: "first" }]);
543
+ await notify({ type: "application", marker: "reactivated" });
544
+ assert.deepEqual((await events()).slice(5).map(({ reader, marker }) => [reader, marker]), [["first", "reactivated"]]);
545
+ assert.equal((await socket()).calls.length, initialCalls, "Changing presentation readers does not restart the retained subscription");
546
+ await readers([]);
547
+ await notify({ type: "application", marker: "voice-only" });
548
+ assert.equal((await events()).length, 6, "Runtime retention does not retain a released view's callback");
549
+ assert.equal((await socket()).active, 1);
550
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).turns[0].assistant.text, "Applied before notification");
551
+ await f.page.evaluate(() => window.conversationFixture.releaseVoice());
552
+ await expect.poll(async () => (await socket()).active).toBe(0);
553
+ assert.equal((await socket()).listeners, 0);
554
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 0);
555
+ });
556
+
557
+ test("canonical receipts settle a held response and uncertain delivery stays inspection-only through reconnect", options, async t => {
558
+ const f = await fixture(t, 390);
559
+ await f.command("mode", { mode: "hold" });
560
+ await f.input.fill("Receipt before response");
561
+ await f.input.press("Enter");
562
+ await expect.poll(async () => (await f.state()).held).toBe(1);
563
+ await f.command("accept", { holdResponse: true });
564
+ await f.notify("chat:1");
565
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().delivery.state.sending)).toBe(false);
566
+ await expect(f.primary.getByText("Receipt before response", { exact: true })).toHaveCount(1);
567
+ assert.equal((await f.state()).held, 1, "The canonical receipt wins before the HTTP response is released");
568
+ await f.command("release-response");
569
+ await f.command("finish", { id: "chat:1" });
570
+ await f.notify("chat:1");
571
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
572
+ await f.command("mode", { mode: "uncertain" });
573
+ await f.input.fill("Keep this exact uncertain request");
574
+ await f.input.press("Enter");
575
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
576
+ await expect(check).toBeVisible();
577
+ await expect(f.primary.getByText("Keep this exact uncertain request", { exact: true })).toHaveCount(1);
578
+ await expect(f.primary.getByRole("button", { name: /^(Retry|Edit|Cancel)$/u })).toHaveCount(0);
579
+ const checkBox = await check.boundingBox();
580
+ assert.ok(checkBox.width >= 48 && checkBox.height >= 48);
581
+ await f.input.fill("Preserve the newer draft");
582
+ await expect(f.primary.getByRole("button", { name: "Send message", exact: true })).toBeDisabled();
583
+ await check.click();
584
+ await expect(check).toBeEnabled();
585
+ assert.equal((await f.sent()).length, 2);
586
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
587
+ await expect(check).toBeVisible();
588
+ await expect(f.input).toHaveValue("Preserve the newer draft");
589
+ await expect(f.primary.getByText("Keep this exact uncertain request", { exact: true })).toHaveCount(1);
590
+ assert.equal((await f.sent()).length, 2, "Reconnect only subscribes and reads");
591
+ await f.command("confirm", { id: "chat:1" });
592
+ await check.click();
593
+ await expect(check).toHaveCount(0);
594
+ await expect(f.primary.getByText("Keep this exact uncertain request", { exact: true })).toHaveCount(1);
595
+ assert.equal((await f.sent()).length, 2, "Explicit inspection does not submit the request");
596
+ await f.command("deny", { denied: true });
597
+ await f.notify("chat:1");
598
+ await expect(f.primary.getByText("Access denied.", { exact: true })).toBeVisible();
599
+ await expect(f.input).toBeDisabled();
600
+ await expect(f.input).toHaveValue("");
601
+ await expect(f.primary.getByText("Keep this exact uncertain request", { exact: true })).toHaveCount(0);
602
+ });
603
+
604
+ test("an explicit pre-admission rejection retains original retry identity and the newer draft", options, async t => {
605
+ const f = await fixture(t);
606
+ await f.command("mode", { mode: "rejected" });
607
+ await f.input.fill("Retry only this original message");
608
+ await f.input.press("Enter");
609
+ await expect(f.primary.getByText("Failed: The message was rejected before admission.", { exact: true })).toBeVisible();
610
+ await f.input.fill("My newer draft");
611
+ await f.command("mode", { mode: "accepted" });
612
+ await f.primary.getByRole("button", { name: "Retry", exact: true }).click();
613
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
614
+ await expect(f.input).toHaveValue("My newer draft");
615
+ const requests = await f.sent();
616
+ assert.equal(requests.length, 2);
617
+ assert.deepEqual(requests[0].input, requests[1].input);
618
+ await expect(f.primary.getByText("Retry only this original message", { exact: true })).toHaveCount(1);
619
+ await f.primary.getByRole("button", { name: "Stop", exact: true }).click();
620
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
621
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 1);
622
+ });
623
+
624
+ test("authored application data stays captured across retry, a lost receipt and changed focus", options, async t => {
625
+ const f = await fixture(t);
626
+ const originalData = { clientId: "typed-client", focus: { project: "first", session: "original" } };
627
+ await f.page.evaluate(data => window.conversationFixture.data(data), originalData);
628
+ await f.command("mode", { mode: "rejected" });
629
+ await f.input.fill("Keep this authored context");
630
+ await f.input.press("Enter");
631
+ await expect(f.primary.getByText("Failed: The message was rejected before admission.", { exact: true })).toBeVisible();
632
+ const [original] = await f.sent();
633
+ assert.deepEqual(original.input.data, originalData);
634
+ assert.deepEqual(Object.keys(original.input).sort(), ["data", "messageId", "text"]);
635
+
636
+ await f.page.evaluate(() => window.conversationFixture.changeFocus({ project: "second", session: "new-selection" }));
637
+ await f.input.fill("Keep the newer typed draft");
638
+ await f.command("mode", { mode: "uncertain" });
639
+ await f.primary.getByRole("button", { name: "Retry", exact: true }).click();
640
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
641
+ await expect(check).toBeVisible();
642
+ assert.deepEqual((await f.sent())[1].input, original.input);
643
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.current().delivery.state.messages[0].payload.request.data), originalData);
644
+ assert.deepEqual((await f.state()).states["chat:1"].pendingRequest.data, originalData);
645
+
646
+ await f.page.evaluate(() => {
647
+ window.conversationFixture.changeFocus({ project: "third", session: "after-receipt-loss" });
648
+ window.conversationFixture.socket.reconnect();
649
+ });
650
+ await expect(check).toBeVisible();
651
+ await check.click();
652
+ await expect(check).toBeEnabled();
653
+ assert.equal((await f.sent()).length, 2, "Receipt inspection and reconnect do not reconstruct or resend input data");
654
+ await f.command("confirm", { id: "chat:1" });
655
+ await check.click();
656
+ await expect(check).toHaveCount(0);
657
+ await expect(f.input).toHaveValue("Keep the newer typed draft");
658
+ assert.equal((await f.sent()).length, 2);
659
+ });
660
+
661
+ test("voice supplies its own authored data on the retained target and cannot send after actor retirement", options, async t => {
662
+ const f = await fixture(t);
663
+ await f.page.evaluate(() => {
664
+ window.conversationFixture.data({ clientId: "typed-client", focus: { project: "typed-original" } });
665
+ window.conversationFixture.retainVoice();
666
+ });
667
+ await f.input.fill("A typed draft remains independent");
668
+ const voiceData = { clientId: "voice-client", focus: { project: "voice-capture", session: "voice-original" } };
669
+ const result = await f.page.evaluate(async data => {
670
+ const pending = window.conversationFixture.sendVoice("Use the utterance capture", data);
671
+ data.focus.project = "changed-after-submission";
672
+ return pending;
673
+ }, voiceData);
674
+ assert.equal(result.status, "accepted");
675
+ await expect(f.input).toHaveValue("A typed draft remains independent");
676
+ const [request] = await f.sent();
677
+ assert.equal(request.id, "chat:1");
678
+ assert.deepEqual(request.input.data, voiceData);
679
+ await f.command("finish", { id: "chat:1" });
680
+ await f.notify("chat:1");
681
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
682
+
683
+ await f.page.evaluate(() => {
684
+ window.conversationFixture.target("chat:2");
685
+ window.conversationFixture.changeFocus({ project: "new-text-target" });
686
+ });
687
+ await expect(f.input).toBeEnabled();
688
+ await f.input.fill("Another target's draft");
689
+ const retainedData = { clientId: "voice-client", focus: { project: "second-utterance" } };
690
+ assert.equal((await f.page.evaluate(data => window.conversationFixture.sendVoice("Still the retained conversation", data), retainedData)).status, "accepted");
691
+ assert.equal((await f.sent())[1].id, "chat:1");
692
+ assert.deepEqual((await f.sent())[1].input.data, retainedData);
693
+ await expect(f.input).toHaveValue("Another target's draft");
694
+
695
+ await f.page.evaluate(() => window.conversationFixture.actor("43"));
696
+ await expect.poll(async () => (await f.page.evaluate(() => window.conversationFixture.voiceState())).current).toBe(false);
697
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("Retired actor", { focus: { project: "forbidden" } })), false);
698
+ const retired = await f.page.evaluate(() => window.conversationFixture.voiceState());
699
+ assert.equal(retired.draft, "");
700
+ assert.deepEqual(retired.turns, []);
701
+ assert.equal((await f.sent()).length, 2);
702
+ });
703
+
704
+ test("canonical questions require complete answers and keep the captured reply while voice retains a pending send", options, async t => {
705
+ const f = await fixture(t, 390);
706
+ const prompt = "Please answer both.\n[1] Which file?\n[2] Which helper?";
707
+ await f.command("question", { id: "chat:1", text: prompt, pending: true });
708
+ await f.notify("chat:1");
709
+ await f.page.evaluate(() => window.conversationFixture.questions(true));
710
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
711
+ const first = f.primary.getByRole("textbox", { name: "[1] Which file?", exact: true });
712
+ const second = f.primary.getByRole("textbox", { name: "[2] Which helper?", exact: true });
713
+ await expect(first, "An unfinished assistant reply does not open a question form").toHaveCount(0);
714
+ await f.command("question", { id: "chat:1", text: prompt });
715
+ await f.notify("chat:1");
716
+ await expect(first).toBeVisible();
717
+ const send = f.primary.getByRole("button", { name: "Send message", exact: true });
718
+ await expect(send).toBeDisabled();
719
+ await first.fill("src/main.js");
720
+ await expect(send).toBeDisabled();
721
+ await f.page.evaluate(() => window.conversationFixture.mirror(true));
722
+ const mirroredFirst = f.page.locator("[data-mirror]").getByRole("textbox", { name: "[1] Which file?", exact: true });
723
+ await expect(mirroredFirst).toHaveValue("src/main.js");
724
+ await mirroredFirst.fill("src/shared.js");
725
+ await expect(first).toHaveValue("src/shared.js");
726
+ await first.fill("src/main.js");
727
+ await f.page.evaluate(() => {
728
+ window.conversationFixture.retainVoice();
729
+ window.conversationFixture.mirror(false);
730
+ window.conversationFixture.show(false);
731
+ });
732
+ await expect(f.primary).toHaveCount(0);
733
+ await f.page.evaluate(() => window.conversationFixture.show(true));
734
+ await expect(first, "The shared form retains an unfinished answer while only voice holds the conversation").toHaveValue("src/main.js");
735
+ await expect(send).toBeDisabled();
736
+ await second.fill("parseInput");
737
+ await f.input.fill("Keep the existing behavior.");
738
+ await expect(send).toBeEnabled();
739
+ for (const width of [390, 800, 1365]) {
740
+ await f.page.setViewportSize({ width, height: 900 });
741
+ await expect(first).toBeVisible();
742
+ await expect(second).toBeVisible();
743
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
744
+ }
745
+ await f.command("mode", { mode: "hold" });
746
+ await send.click();
747
+ await expect.poll(async () => (await f.state()).held).toBe(1);
748
+ await expect(first).toHaveCount(0);
749
+ await f.input.fill("My next thought stays editable.");
750
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.show(false); });
751
+ await expect(f.primary).toHaveCount(0);
752
+ await f.page.evaluate(() => window.conversationFixture.show(true));
753
+ await expect(f.input).toHaveValue("My next thought stays editable.");
754
+ await expect(first, "The retained send does not reopen its submitted form").toHaveCount(0);
755
+ const [request] = await f.sent();
756
+ assert.deepEqual(Object.keys(request.input).sort(), ["messageId", "text"]);
757
+ assert.equal(request.input.text, "[1] src/main.js\n[2] parseInput\n\nKeep the existing behavior.");
758
+ await f.command("accept", { holdResponse: true });
759
+ await f.notify("chat:1");
760
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().delivery.state.sending)).toBe(false);
761
+ assert.equal((await f.state()).held, 1);
762
+ await f.command("release-response");
763
+ await expect(f.input).toHaveValue("My next thought stays editable.");
764
+ assert.equal((await f.sent()).length, 1);
765
+ });
766
+
767
+ test("canonical questions retry their exact formatted reply and uncertain delivery stays inspection-only", options, async t => {
768
+ const f = await fixture(t);
769
+ await f.command("question", { id: "chat:1", text: "[1] Which file should change?" });
770
+ await f.notify("chat:1");
771
+ await f.page.evaluate(() => window.conversationFixture.questions(true));
772
+ const answer = f.primary.getByRole("textbox", { name: "[1] Which file should change?", exact: true });
773
+ await answer.fill("src/first.js");
774
+ await f.input.fill("Use the existing helper.");
775
+ await f.command("mode", { mode: "rejected" });
776
+ await f.primary.getByRole("button", { name: "Send message", exact: true }).click();
777
+ await expect(f.primary.getByText("Failed: The message was rejected before admission.", { exact: true })).toBeVisible();
778
+ await expect(answer).toHaveCount(0);
779
+ await f.input.fill("A separate newer thought.");
780
+ await f.command("mode", { mode: "uncertain" });
781
+ await f.primary.getByRole("button", { name: "Retry", exact: true }).click();
782
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
783
+ await expect(check).toBeVisible();
784
+ const requests = await f.sent();
785
+ assert.equal(requests.length, 2);
786
+ assert.deepEqual(requests[1].input, requests[0].input);
787
+ assert.equal(requests[0].input.text, "[1] src/first.js\n\nUse the existing helper.");
788
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
789
+ await expect(check).toBeVisible();
790
+ await check.click();
791
+ await expect(check).toBeEnabled();
792
+ assert.equal((await f.sent()).length, 2);
793
+ await f.command("confirm", { id: "chat:1" });
794
+ await check.click();
795
+ await expect(check).toHaveCount(0);
796
+ await expect(f.input).toHaveValue("A separate newer thought.");
797
+ await expect(answer).toHaveCount(0);
798
+ assert.equal((await f.sent()).length, 2);
799
+ });
800
+
801
+ test("canonical questions preserve choice values and free-form escape while isolating target and actor changes", options, async t => {
802
+ const f = await fixture(t);
803
+ await f.command("question", { id: "chat:1", text: ["[1] Include callbacks?", "Possible answers:",
804
+ "- Complete lifecycle (Recommended)", "- Sending first", "[2] Which file?"].join("\n") });
805
+ await f.notify("chat:1");
806
+ await f.page.evaluate(() => window.conversationFixture.questions(true));
807
+ const select = f.primary.getByRole("combobox", { name: "[1] Include callbacks?", exact: true });
808
+ await f.primary.locator(".assistant-questions__question-select .v-field__input").click();
809
+ await expect(f.page.getByRole("option", { name: "I am not sure", exact: true })).toHaveCount(0);
810
+ await select.press("Escape");
811
+ await f.page.evaluate(() => window.conversationFixture.questions({
812
+ extraChoice: { label: "I am not sure", selectLabel: "I am not sure", value: "I am not sure", recommended: false }
813
+ }));
814
+ await f.primary.locator(".assistant-questions__question-select .v-field__input").click();
815
+ await f.page.getByRole("option", { name: "I am not sure", exact: true }).click();
816
+ await f.primary.getByRole("textbox", { name: "[2] Which file?", exact: true }).fill("src/main.js");
817
+ await f.input.fill("Let me explain in my own words.");
818
+ await f.primary.getByRole("button", { name: "Answer normally instead", exact: true }).click();
819
+ await expect(select).toHaveCount(0);
820
+ await expect(f.input).toHaveValue("Let me explain in my own words.");
821
+ await f.primary.getByRole("button", { name: "Send message", exact: true }).click();
822
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
823
+ assert.equal((await f.sent())[0].input.text, "Let me explain in my own words.");
824
+ await f.command("finish", { id: "chat:1" });
825
+ await f.notify("chat:1");
826
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
827
+ await f.command("question", { id: "chat:1", text: "Choose.\nPossible answers:\n- Option A: Use A.\n- Option B: Use B." });
828
+ await f.notify("chat:1");
829
+ await f.primary.locator(".assistant-questions__answer-choices").getByText("Option A", { exact: true }).click();
830
+ assert.equal((await f.sent()).length, 1, "Choosing an answer does not submit it");
831
+ await f.primary.getByRole("button", { name: "Send message", exact: true }).click();
832
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
833
+ assert.equal((await f.sent())[1].input.text, "Use A.");
834
+ await f.command("finish", { id: "chat:1" });
835
+ await f.notify("chat:1");
836
+ const samePrompt = "[1] Which subsystem?";
837
+ await f.command("question", { id: "chat:1", text: samePrompt });
838
+ await f.command("question", { id: "chat:2", text: samePrompt });
839
+ await f.notify("chat:1");
840
+ const field = f.primary.getByRole("textbox", { name: samePrompt, exact: true });
841
+ await field.fill("Old target answer");
842
+ await f.page.evaluate(() => window.conversationFixture.target("chat:2"));
843
+ await expect(field).toHaveValue("");
844
+ await field.fill("Old actor answer");
845
+ await f.page.evaluate(() => window.conversationFixture.actor("43"));
846
+ await expect(field).toHaveValue("");
847
+ await f.command("deny", { denied: true });
848
+ await f.notify("chat:2");
849
+ await expect(f.primary.getByText("Access denied.", { exact: true })).toBeVisible();
850
+ await expect(field).toHaveCount(0);
851
+ assert.equal((await f.sent()).length, 2);
852
+ });
853
+
854
+ test("canonical model controls use the supplied reactive owner and disappear with unavailable or retired access", options, async t => {
855
+ const f = await fixture(t, 390);
856
+ const choose = f.primary.getByRole("button", { name: "Choose AI", exact: true });
857
+ await expect(choose).toHaveCount(0);
858
+ await f.input.fill("Keep this independent draft.");
859
+ await f.page.evaluate(() => window.conversationFixture.models(true));
860
+ await choose.click();
861
+ const apply = f.page.getByRole("button", { name: "Apply", exact: true });
862
+ await expect(apply).toBeDisabled();
863
+ await f.page.getByRole("button", { name: "Large model", exact: true }).click();
864
+ await expect(apply).toBeEnabled();
865
+ for (const width of [390, 800, 1365]) {
866
+ await f.page.setViewportSize({ width, height: 900 });
867
+ await expect(apply).toBeVisible();
868
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
869
+ }
870
+ await apply.click();
871
+ const applying = f.page.getByRole("button", { name: "Applying…", exact: true });
872
+ await expect(applying).toBeDisabled();
873
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.modelState()), {
874
+ selected: "large", applied: "small", saving: true, selections: ["large"]
875
+ });
876
+ await f.notify("chat:1");
877
+ await expect(applying).toBeDisabled();
878
+ await expect(f.input).toHaveValue("Keep this independent draft.");
879
+ await f.page.evaluate(() => window.conversationFixture.finishModelApply());
880
+ await expect(applying).toHaveCount(0);
881
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.modelState()), {
882
+ selected: "large", applied: "large", saving: false, selections: ["large"]
883
+ });
884
+ await choose.click();
885
+ await f.page.evaluate(() => window.conversationFixture.modelError("Reconnect the application catalogue."));
886
+ await expect(f.page.getByText("Reconnect the application catalogue.", { exact: true })).toBeVisible();
887
+ await f.page.getByRole("button", { name: "Try again", exact: true }).click();
888
+ await expect(f.page.getByText("Reconnect the application catalogue.", { exact: true })).toHaveCount(0);
889
+ await f.page.keyboard.press("Escape");
890
+ await f.input.press("Enter");
891
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
892
+ await choose.click();
893
+ await expect(f.page.getByRole("button", { name: "Small model", exact: true })).toBeDisabled();
894
+ await expect(f.page.getByText("AI choices are view-only while the assistant is working.", { exact: true })).toBeVisible();
895
+ await f.command("deny", { denied: true });
896
+ await f.notify("chat:1");
897
+ await expect(f.primary.getByText("Access denied.", { exact: true })).toBeVisible();
898
+ await expect(choose).toHaveCount(0);
899
+ await expect(f.page.getByRole("button", { name: "Small model", exact: true })).toHaveCount(0);
900
+ await f.command("deny", { denied: false });
901
+ await f.notify("chat:1");
902
+ await expect(choose).toBeVisible();
903
+ await f.page.evaluate(() => window.conversationFixture.actor(""));
904
+ await expect(choose).toHaveCount(0);
905
+ assert.deepEqual((await f.page.evaluate(() => window.conversationFixture.modelState())).selections, ["large"]);
906
+ assert.equal((await f.sent()).length, 1);
907
+ assert.equal((await f.state()).requests.some(request => request.suffix && request.suffix !== "/messages"), false);
908
+ });
909
+
910
+ test("accepted draft clearing follows canonical receipts and nonqueued admission rejects overlap before dispatch", options, async t => {
911
+ const f = await fixture(t, 390, { acceptedDraft: true });
912
+ await f.command("steering", { id: "chat:1", enabled: true });
913
+ await f.notify("chat:1");
914
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.capabilities.steering)).toBe(true);
915
+ await f.page.evaluate(() => window.conversationFixture.attachments(true));
916
+ await f.primary.locator("input[type=file]").setInputFiles({ name: "accepted.txt", mimeType: "text/plain", buffer: Buffer.from("Keep until accepted") });
917
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.attachmentState().ready.length)).toBe(1);
918
+ const [attachment] = (await f.page.evaluate(() => window.conversationFixture.attachmentState())).ready;
919
+ await f.command("mode", { mode: "hold" });
920
+ await f.input.fill("Keep this draft until accepted.");
921
+ await f.input.press("Enter");
922
+ await expect.poll(async () => (await f.state()).held).toBe(1);
923
+ await expect(f.input).toHaveValue("Keep this draft until accepted.");
924
+ await expect(f.input).toBeDisabled();
925
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.mirror(true); });
926
+ await expect(f.page.locator("[data-mirror]").getByRole("textbox", { name: "Message AI assistant" })).toHaveValue("Keep this draft until accepted.");
927
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
928
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("Do not overlap")), false);
929
+ assert.equal((await f.sent()).length, 1);
930
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.dispatches())).length, 1);
931
+ await f.command("accept", { holdResponse: true });
932
+ await f.notify("chat:1");
933
+ await expect(f.input).toBeEnabled();
934
+ await expect(f.input).toHaveValue("");
935
+ assert.equal((await f.state()).held, 1, "Canonical acceptance clears the draft before the original HTTP response");
936
+ assert.deepEqual((await f.page.evaluate(() => window.conversationFixture.attachmentState())).acknowledged, [[attachment.attachmentId]]);
937
+ await f.input.fill("New request while the assistant is working.");
938
+ await f.command("release-response");
939
+ await expect(f.input).toHaveValue("New request while the assistant is working.");
940
+ await f.input.press("Enter");
941
+ await expect.poll(async () => (await f.state()).held).toBe(1);
942
+ assert.equal((await f.sent())[1].input.steer, true, "The host's existing working-state admission remains available");
943
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("No queued overlap while working")), false);
944
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.dispatches())).length, 2);
945
+ await f.page.evaluate(() => { window.conversationFixture.current().draft.value = "Keep this newer draft."; });
946
+ await f.command("accept");
947
+ await expect(f.input).toBeEnabled();
948
+ await expect(f.input).toHaveValue("Keep this newer draft.");
949
+ assert.equal((await f.sent()).length, 2);
950
+ });
951
+
952
+ test("accepted draft retries use only the current authored entry and an older acceptance cannot clear its replacement", options, async t => {
953
+ const f = await fixture(t, 1280, { acceptedDraft: true });
954
+ await f.command("steering", { id: "chat:1", enabled: true });
955
+ await f.notify("chat:1");
956
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.capabilities.steering)).toBe(true);
957
+ await f.page.evaluate(() => window.conversationFixture.data({ clientId: "typed", focus: { project: "first" } }));
958
+ async function submit(message) {
959
+ const count = (await f.sent()).length;
960
+ await f.input.fill(message);
961
+ await f.input.press("Enter");
962
+ await expect.poll(async () => (await f.sent()).length).toBe(count + 1);
963
+ await expect(f.input).toBeEnabled();
964
+ return (await f.sent()).at(-1);
965
+ }
966
+ await f.command("mode", { mode: "rejected" });
967
+ const first = await submit("Earlier failed request");
968
+ await expect(f.input).toHaveValue("Earlier failed request");
969
+ await f.page.evaluate(() => window.conversationFixture.changeFocus({ project: "second" }));
970
+ const second = await submit("Current failed request");
971
+ assert.notEqual(second.input.messageId, first.input.messageId);
972
+ await f.page.evaluate(() => window.conversationFixture.changeFocus({ project: "changed-before-retry" }));
973
+ await f.command("mode", { mode: "accepted" });
974
+ const secondRetry = await submit("Current failed request");
975
+ assert.deepEqual(secondRetry.input, second.input, "Ordinary Send reuses the authored ID and data after pre-admission failure");
976
+ await expect(f.input).toHaveValue("");
977
+
978
+ await f.command("mode", { mode: "rejected" });
979
+ const replacement = await submit("Earlier failed request");
980
+ assert.notEqual(replacement.input.messageId, first.input.messageId, "An old matching failure is not the current authored submission");
981
+ await f.command("mode", { mode: "accepted" });
982
+ await f.primary.getByRole("button", { name: "Retry", exact: true }).first().click();
983
+ await expect.poll(async () => (await f.sent()).length).toBe(5);
984
+ await expect(f.input).toBeEnabled();
985
+ assert.deepEqual((await f.sent())[4].input, first.input);
986
+ await expect(f.input, "Accepting the older identical text cannot clear the replacement draft").toHaveValue("Earlier failed request");
987
+ await f.page.evaluate(() => window.conversationFixture.changeFocus({ project: "changed-again" }));
988
+ const replacementRetry = await submit("Earlier failed request");
989
+ assert.deepEqual(replacementRetry.input, replacement.input);
990
+ await expect(f.input).toHaveValue("");
991
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.dispatches())).length, 6);
992
+ });
993
+
994
+ test("accepted draft identity survives voice retention and lost receipts without voice consuming the typed capture", options, async t => {
995
+ const f = await fixture(t, 1280, { acceptedDraft: true });
996
+ await f.command("steering", { id: "chat:1", enabled: true });
997
+ await f.notify("chat:1");
998
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.capabilities.steering)).toBe(true);
999
+ const typedData = { clientId: "typed", focus: { project: "original" } };
1000
+ await f.page.evaluate(data => window.conversationFixture.data(data), typedData);
1001
+ await f.command("mode", { mode: "rejected" });
1002
+ await f.input.fill("Keep the typed capture.");
1003
+ await f.input.press("Enter");
1004
+ await expect.poll(async () => (await f.sent()).length).toBe(1);
1005
+ await expect(f.input).toBeEnabled();
1006
+ const [typed] = await f.sent();
1007
+ await f.page.evaluate(() => {
1008
+ window.conversationFixture.retainVoice();
1009
+ window.conversationFixture.show(false);
1010
+ window.conversationFixture.changeFocus({ project: "current" });
1011
+ });
1012
+ await expect(f.primary).toHaveCount(0);
1013
+ await f.command("mode", { mode: "hold" });
1014
+ const voiceData = { clientId: "voice", focus: { project: "utterance" } };
1015
+ await f.page.evaluate(data => { window.voiceSubmission = window.conversationFixture.sendVoice("Independent voice", data); }, voiceData);
1016
+ await expect.poll(async () => (await f.state()).held).toBe(1);
1017
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("No voice-only overlap")), false);
1018
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.dispatches())).length, 2);
1019
+ await f.command("accept");
1020
+ assert.equal((await f.page.evaluate(() => window.voiceSubmission)).status, "accepted");
1021
+ assert.deepEqual((await f.sent())[1].input.data, voiceData);
1022
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, "Keep the typed capture.");
1023
+ await f.page.evaluate(() => window.conversationFixture.show(true));
1024
+ await expect(f.input).toBeEnabled();
1025
+ await expect(f.input).toHaveValue("Keep the typed capture.");
1026
+ await f.command("mode", { mode: "uncertain" });
1027
+ await f.input.press("Enter");
1028
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1029
+ await expect(check).toBeVisible();
1030
+ assert.deepEqual((await f.sent())[2].input, typed.input);
1031
+ await expect(f.input).toHaveValue("Keep the typed capture.");
1032
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1033
+ await expect(check).toBeVisible();
1034
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1035
+ await check.click();
1036
+ await expect(check).toBeEnabled();
1037
+ assert.equal((await f.sent()).length, 3);
1038
+ await f.command("confirm", { id: "chat:1" });
1039
+ await check.click();
1040
+ await expect(check).toHaveCount(0);
1041
+ await expect(f.input).toHaveValue("");
1042
+ assert.equal((await f.sent()).length, 3, "Inspection proves acceptance without recreating the typed request");
1043
+ });
1044
+
1045
+ test("canonical presentation options preserve product labels and show a live voice preview without changing history", options, async t => {
1046
+ const f = await fixture(t, 390, { acceptedDraft: true });
1047
+ const display = { assistantLabel: "Ada", systemLabel: "Vibe64", variant: "task", visible: true,
1048
+ userMessageFormat: "plain", progressPreviewLimit: 0, ariaLabel: "Message Ada", submitAriaLabel: "Send to Ada",
1049
+ submitLabel: "Send to Ada", rows: 1, layout: "compact", placeholder: "Talk it through with Ada…",
1050
+ welcomeMessage: "Discuss an idea with Ada." };
1051
+ const present = value => f.page.evaluate(value => window.conversationFixture.presentation(value), value);
1052
+ await present(display);
1053
+ const input = f.primary.getByRole("textbox", { name: "Message Ada", exact: true });
1054
+ await expect(input).toHaveAttribute("rows", "1");
1055
+ await expect(input).toHaveAttribute("placeholder", "Talk it through with Ada…");
1056
+ await expect(f.primary.getByText("Discuss an idea with Ada.", { exact: true })).toBeVisible();
1057
+ await expect(f.primary.locator(".assistant-transcript--task")).toBeVisible();
1058
+ await input.fill("My independent typed draft");
1059
+ const send = f.primary.getByRole("button", { name: "Send to Ada", exact: true });
1060
+ await expect(send).toHaveText("Send to Ada");
1061
+ assert.deepEqual(await f.page.evaluate(() => {
1062
+ const { conversation, composer } = window.conversationFixture.adapter();
1063
+ return [conversation.assistantLabel, conversation.systemLabel, conversation.progressPreviewLimit, composer.rows];
1064
+ }), ["Ada", "Vibe64", 0, 1]);
1065
+ const previewMessage = { id: "live-voice", text: "I meant **tea**" };
1066
+ await present({ ...display, previewMessage });
1067
+ await expect(f.primary.locator(".assistant-transcript__plain-user")).toHaveText("I meant **tea**");
1068
+ await expect(f.primary.getByText("Pending", { exact: true })).toHaveCount(1);
1069
+ await expect(f.primary.getByText("Discuss an idea with Ada.", { exact: true })).toHaveCount(0);
1070
+ await present({ ...display, previewMessage: { ...previewMessage, text: "I meant **tea** with sugar" } });
1071
+ await expect(f.primary.locator(".assistant-transcript__plain-user")).toHaveText("I meant **tea** with sugar");
1072
+ assert.deepEqual(await f.page.evaluate(() => {
1073
+ const current = window.conversationFixture.current();
1074
+ return [current.turns.value, current.snapshot.value.conversationLog, current.delivery.state.messages];
1075
+ }), [[], [], []], "Live transcription is outside saved history and receipt state");
1076
+ await present({ ...display, visible: false, previewMessage });
1077
+ await expect(f.primary.getByRole("region", { name: "Conversation history" })).toHaveCount(0);
1078
+ await expect(input).toHaveValue("My independent typed draft");
1079
+ await present({ ...display, previewMessage });
1080
+ for (const width of [390, 800, 1365]) {
1081
+ await f.page.setViewportSize({ width, height: 900 });
1082
+ await expect(f.primary.getByText("I meant **tea**", { exact: true })).toBeVisible();
1083
+ await expect(send).toBeVisible();
1084
+ if (width === 390) { const box = await send.boundingBox(); assert.ok(box.width >= 48 && box.height >= 48); }
1085
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
1086
+ }
1087
+ await present({ ...display, previewMessage: null });
1088
+ await expect(f.primary.getByText("I meant **tea**", { exact: true })).toHaveCount(0);
1089
+ await expect(f.primary.getByText("Discuss an idea with Ada.", { exact: true })).toBeVisible();
1090
+ await expect(input).toHaveValue("My independent typed draft");
1091
+ assert.equal((await f.sent()).length, 0);
1092
+ });
1093
+
1094
+ test("a voice preview cannot acknowledge sending or uncertain delivery and deduplicates only against real receipts", options, async t => {
1095
+ const f = await fixture(t, 1280, { acceptedDraft: true });
1096
+ const preview = (messageId, text) => f.page.evaluate(value => window.conversationFixture.presentation({
1097
+ systemLabel: "Vibe64", previewMessage: value
1098
+ }), { messageId, text });
1099
+ const delivery = () => f.page.evaluate(() => {
1100
+ const current = window.conversationFixture.current();
1101
+ return { sending: current.delivery.state.sending, messages: current.delivery.state.messages.map(({ id, status }) => ({ id, status })),
1102
+ turns: current.turns.value, history: current.snapshot.value.conversationLog };
1103
+ });
1104
+ await f.command("mode", { mode: "hold" });
1105
+ await f.input.fill("Wait for a real receipt.");
1106
+ await f.input.press("Enter");
1107
+ await expect.poll(async () => (await f.state()).held).toBe(1);
1108
+ const first = (await f.sent())[0].input.messageId;
1109
+ await preview(first, "The unsent preview must not replace pending input.");
1110
+ await expect(f.primary.getByText("Wait for a real receipt.", { exact: true })).toHaveCount(1);
1111
+ await expect(f.primary.getByText("The unsent preview must not replace pending input.", { exact: true })).toHaveCount(0);
1112
+ assert.deepEqual(await delivery(), { sending: true, messages: [{ id: first, status: "pending" }], turns: [], history: [] });
1113
+ await preview("another-live-utterance", "Still speaking before admission");
1114
+ await expect(f.primary.getByText("Still speaking before admission", { exact: true })).toHaveCount(1);
1115
+ assert.equal((await delivery()).sending, true);
1116
+ assert.equal((await delivery()).messages.length, 1);
1117
+ assert.deepEqual((await delivery()).turns, []);
1118
+ await f.command("accept");
1119
+ await expect(f.input).toBeEnabled();
1120
+ await preview(first, "Wait for a real receipt.");
1121
+ await expect.poll(async () => (await delivery()).history.length).toBe(1);
1122
+ await expect(f.primary.getByText("Wait for a real receipt.", { exact: true })).toHaveCount(1);
1123
+ await expect(f.primary.getByText("Pending", { exact: true })).toHaveCount(0);
1124
+ await f.command("finish", { id: "chat:1" });
1125
+ await f.notify("chat:1");
1126
+ await expect(f.primary.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0);
1127
+ await f.command("mode", { mode: "uncertain" });
1128
+ await f.input.fill("Keep this uncertain request.");
1129
+ await f.input.press("Enter");
1130
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1131
+ await expect(check).toBeVisible();
1132
+ const second = (await f.sent())[1].input.messageId;
1133
+ await preview(second, "An uncertain preview is not an acknowledgement.");
1134
+ await expect(f.primary.getByText("An uncertain preview is not an acknowledgement.", { exact: true })).toHaveCount(0);
1135
+ await expect(f.primary.getByText("Vibe64", { exact: true })).toBeVisible();
1136
+ assert.deepEqual((await delivery()).messages, [{ id: second, status: "uncertain" }]);
1137
+ assert.equal((await delivery()).history.length, 1);
1138
+ await check.click();
1139
+ await expect(check).toBeEnabled();
1140
+ assert.deepEqual((await delivery()).messages, [{ id: second, status: "uncertain" }]);
1141
+ await expect(f.input).toHaveValue("Keep this uncertain request.");
1142
+ await f.command("confirm", { id: "chat:1" });
1143
+ await check.click();
1144
+ await expect(check).toHaveCount(0);
1145
+ await expect(f.input).toHaveValue("");
1146
+ await expect(f.primary.getByText("Keep this uncertain request.", { exact: true })).toHaveCount(1);
1147
+ await expect(f.primary.getByText("An uncertain preview is not an acknowledgement.", { exact: true })).toHaveCount(0);
1148
+ assert.equal((await f.sent()).length, 2);
1149
+ });
1150
+
1151
+
1152
+ test("an interim reply stays on its exact turn without acknowledging delivery and restores from the canonical read", options, async t => {
1153
+ const f = await fixture(t, 390, { acceptedDraft: true });
1154
+ const view = () => f.page.evaluate(() => {
1155
+ const current = window.conversationFixture.current();
1156
+ return { messages: current.delivery.state.messages.map(({ id, status }) => ({ id, status })),
1157
+ turns: current.turns.value, history: current.snapshot.value.conversationLog,
1158
+ interimReply: current.snapshot.value.interimReply };
1159
+ });
1160
+ const present = reply => f.page.evaluate(interimReply => window.conversationFixture.socket.notify("chat:1", {
1161
+ type: "presentation", interimReply
1162
+ }), reply);
1163
+ await f.command("mode", { mode: "uncertain" });
1164
+ await f.input.fill("Check the project before answering.");
1165
+ await f.input.press("Enter");
1166
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1167
+ await expect(check).toBeVisible();
1168
+ const [{ input: { messageId } }] = await f.sent();
1169
+ const reply = { id: "native-ack", outputId: "native-ack-output", turnId: messageId,
1170
+ role: "assistant", text: "Let me check the project.", status: "completed", autonomous: false };
1171
+ const acknowledgement = f.primary.getByText(reply.text, { exact: true });
1172
+ await present({ ...reply, turnId: "another-turn" });
1173
+ await expect.poll(async () => (await view()).interimReply?.turnId).toBe("another-turn");
1174
+ await expect(acknowledgement).toHaveCount(0);
1175
+ await present(reply);
1176
+ await present(reply);
1177
+ await expect(acknowledgement).toHaveCount(1);
1178
+ await expect(f.primary.locator('[data-message-role="assistant"]').getByText(reply.text, { exact: true })).toHaveCount(1);
1179
+ const uncertain = { messages: [{ id: messageId, status: "uncertain" }], turns: [], history: [], interimReply: reply };
1180
+ assert.deepEqual(await view(), uncertain, "An interim display cannot create an authored receipt or canonical turn");
1181
+ await expect(check).toBeVisible();
1182
+ await expect(f.input).toHaveValue("Check the project before answering.");
1183
+
1184
+ await f.command("interim-reply", { id: "chat:1", reply });
1185
+ const readsBeforeReconnect = (await f.state()).requests.filter(request => request.method === "GET").length;
1186
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1187
+ await expect.poll(async () => (await f.state()).requests.filter(request => request.method === "GET").length)
1188
+ .toBeGreaterThan(readsBeforeReconnect);
1189
+ await expect(acknowledgement).toHaveCount(1);
1190
+ assert.deepEqual(await view(), uncertain);
1191
+ await f.page.evaluate(() => window.conversationFixture.show(false));
1192
+ await expect(f.primary).toHaveCount(0);
1193
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.socket.inspect().active)).toBe(0);
1194
+ await f.page.evaluate(() => window.conversationFixture.show(true));
1195
+ await expect(check).toBeVisible();
1196
+ await expect(acknowledgement).toHaveCount(1);
1197
+ assert.deepEqual(await view(), uncertain, "A reopened reader restores the scalar and pending receipt independently");
1198
+ assert.equal((await f.sent()).length, 1, "Reconnect and remount do not redispatch uncertain input");
1199
+
1200
+ await f.command("confirm", { id: "chat:1" });
1201
+ await f.notify("chat:1");
1202
+ await expect(check).toHaveCount(0);
1203
+ const saved = (await view()).history;
1204
+ assert.equal(saved.length, 1);
1205
+ assert.equal(saved[0].user.messageId, messageId);
1206
+ assert.equal(saved[0].assistant, null);
1207
+ assert.deepEqual((await view()).messages, [], "Only the real canonical authored turn acknowledges delivery");
1208
+ await f.page.evaluate(reply => window.conversationFixture.socket.notify("chat:1", {
1209
+ type: "message", messageId: "native-ack-commentary", outputId: reply.outputId,
1210
+ turnId: reply.turnId, origin: "user", role: "commentary", status: "complete", text: reply.text,
1211
+ streaming: { revision: 1, messages: [] }, interimReply: reply
1212
+ }), reply);
1213
+ await expect(f.primary.locator('[data-message-role="assistant"]').getByText(reply.text, { exact: true })).toHaveCount(1);
1214
+ await expect(f.primary.locator('[data-message-role="commentary"]').getByText(reply.text, { exact: true })).toHaveCount(0);
1215
+ for (const width of [390, 800, 1365]) {
1216
+ await f.page.setViewportSize({ width, height: 900 });
1217
+ await expect(acknowledgement).toHaveCount(1);
1218
+ await expect(acknowledgement).toBeVisible();
1219
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
1220
+ }
1221
+ assert.deepEqual((await view()).history, saved, "Selecting completed commentary for display does not rewrite saved history");
1222
+ assert.deepEqual((await f.state()).states["chat:1"].conversationLog, saved);
1223
+
1224
+ await f.command("finish", { id: "chat:1", text: "The project is ready." });
1225
+ await f.command("interim-reply", { id: "chat:1", reply: null });
1226
+ await f.page.evaluate(turnId => window.conversationFixture.socket.notify("chat:1", {
1227
+ type: "settled", turnId, interimReply: null
1228
+ }), messageId);
1229
+ await expect(f.primary.getByText("The project is ready.", { exact: true })).toHaveCount(1);
1230
+ await expect(acknowledgement).toHaveCount(0);
1231
+ assert.equal((await view()).history[0].assistant.text, "The project is ready.");
1232
+ assert.equal((await view()).interimReply, null);
1233
+ assert.deepEqual((await view()).messages, []);
1234
+ assert.equal((await f.sent()).length, 1);
1235
+ });
1236
+
1237
+ test("draft while loading keeps pre-ID input through an unavailable first read without admitting a message", options, async t => {
1238
+ const f = await fixture(t, 390, { acceptedDraft: true, draftWhileLoading: true, unresolved: true });
1239
+ const send = f.primary.getByRole("button", { name: "Send message", exact: true });
1240
+ await f.input.fill("Keep my first offline draft.");
1241
+ await expect(send).toBeDisabled();
1242
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1243
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current() === null), true);
1244
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).calls.length, 0);
1245
+ assert.deepEqual((await f.state()).requests, []);
1246
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.dispatches()), []);
1247
+ await expect(f.input).toHaveValue("Keep my first offline draft.");
1248
+
1249
+ const reads = "**/api/assistant/home/conversations/*";
1250
+ await f.page.route(reads, route => route.fulfill({ status: 503,
1251
+ json: { error: "Conversation storage is temporarily unavailable." } }));
1252
+ await f.page.evaluate(() => window.conversationFixture.target("chat:1"));
1253
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current()?.error.value))
1254
+ .toBe("Conversation storage is temporarily unavailable.");
1255
+ await expect(f.input).toBeEnabled();
1256
+ await expect(f.input).toHaveValue("Keep my first offline draft.");
1257
+ await f.input.fill("Keep my first offline draft and its later edits.");
1258
+ await expect(send).toBeDisabled();
1259
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1260
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.current().delivery.state.messages), []);
1261
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.dispatches()), []);
1262
+
1263
+ await f.page.unroute(reads);
1264
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1265
+ await expect(send).toBeEnabled();
1266
+ await expect(f.input).toHaveValue("Keep my first offline draft and its later edits.");
1267
+ await send.click();
1268
+ await expect(f.input).toHaveValue("");
1269
+ const requests = await f.sent();
1270
+ assert.equal(requests.length, 1);
1271
+ assert.equal(requests[0].id, "chat:1");
1272
+ assert.equal(requests[0].input.text, "Keep my first offline draft and its later edits.");
1273
+ assert.ok(requests[0].input.messageId);
1274
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.dispatches()), [requests[0].input.messageId]);
1275
+ });
1276
+
1277
+ test("draft while loading resets unresolved and retained input with the real actor and keeps denial closed", options, async t => {
1278
+ const f = await fixture(t, 1280, { acceptedDraft: true, draftWhileLoading: true, unresolved: true });
1279
+ await f.input.fill("Private input for the first actor.");
1280
+ await f.page.evaluate(() => window.conversationFixture.actor("43"));
1281
+ await expect(f.input).toHaveValue("");
1282
+ await f.input.fill("Private input before sign out.");
1283
+ await f.page.evaluate(() => window.conversationFixture.actor(""));
1284
+ await expect(f.input).toHaveValue("");
1285
+ await expect(f.input).toBeDisabled();
1286
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1287
+ await f.page.evaluate(() => window.conversationFixture.actor("44"));
1288
+ await expect(f.input).toBeEnabled();
1289
+ await expect(f.input).toHaveValue("");
1290
+ await f.input.fill("Private input for the resolved target.");
1291
+ await f.page.evaluate(() => window.conversationFixture.target("chat:2"));
1292
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current()?.available.value)).toBe(true);
1293
+ await expect(f.input).toHaveValue("Private input for the resolved target.");
1294
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.actor("45"); });
1295
+ await expect(f.input).toHaveValue("");
1296
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).current, false);
1297
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, "");
1298
+ await f.input.fill("Access-controlled input.");
1299
+ await f.command("deny", { denied: true });
1300
+ await f.notify("chat:2");
1301
+ await expect(f.input).toBeDisabled();
1302
+ await expect(f.input).toHaveValue("");
1303
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1304
+ assert.equal((await f.sent()).length, 0);
1305
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.dispatches()), []);
1306
+ });
1307
+
1308
+ test("draft while loading promotes into an existing retained target without duplicate text or implicit send", options, async t => {
1309
+ const f = await fixture(t, 1280, { acceptedDraft: true, draftWhileLoading: true });
1310
+ await f.input.fill("Already in this target.");
1311
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.target(""); });
1312
+ await expect(f.input).toHaveValue("");
1313
+ await f.input.fill("Typed before identity recovered.");
1314
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.submit()), false);
1315
+ await f.page.evaluate(() => window.conversationFixture.target("chat:1"));
1316
+ const combined = "Already in this target.\nTyped before identity recovered.";
1317
+ await expect(f.input).toHaveValue(combined);
1318
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, combined);
1319
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).active, 1);
1320
+
1321
+ await f.page.evaluate(() => window.conversationFixture.target(""));
1322
+ await f.input.fill(combined);
1323
+ await f.page.evaluate(() => window.conversationFixture.target("chat:1"));
1324
+ await expect(f.input).toHaveValue(combined);
1325
+ await f.page.evaluate(() => window.conversationFixture.target("chat:2"));
1326
+ await expect(f.input).toHaveValue("");
1327
+ await f.input.fill("This belongs only to the second target.");
1328
+ await f.page.evaluate(() => window.conversationFixture.target("chat:1"));
1329
+ await expect(f.input).toHaveValue(combined);
1330
+ assert.equal((await f.sent()).length, 0);
1331
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.dispatches()), []);
1332
+ });
1333
+
1334
+
1335
+ test("paged history uses the original page reader and keeps older messages through live updates", options, async t => {
1336
+ const f = await fixture(t);
1337
+ const turns = Array.from({ length: 5 }, (_, index) => ({
1338
+ turnId: String(index + 1).padStart(6, "0"),
1339
+ user: { role: "user", messageId: `history-${index + 1}`, text: `Saved question ${index + 1}` },
1340
+ ...(index < 4 ? { assistant: { role: "assistant", messageId: `answer-${index + 1}`, text: `Saved answer ${index + 1}` } } : {})
1341
+ }));
1342
+ await f.command("history", { id: "chat:1", turns, limit: 2 });
1343
+ await f.notify("chat:1");
1344
+ await expect(f.primary.getByText("Saved question 5", { exact: true })).toHaveCount(1);
1345
+ await expect(f.primary.getByText("Saved question 1", { exact: true })).toHaveCount(0);
1346
+ await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toBeVisible();
1347
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore({
1348
+ complete: result => { window.historyCompletion = result; }
1349
+ })), true);
1350
+ assert.deepEqual(await f.page.evaluate(() => window.historyCompletion), { changed: true, loaded: true });
1351
+ await expect(f.primary.getByText("Saved question 2", { exact: true })).toHaveCount(1);
1352
+ const paged = (await f.state()).requests.filter(request => request.query.beforeTurnId);
1353
+ assert.deepEqual(paged.map(request => request.query), [{ beforeTurnId: "000004", limit: "2" }]);
1354
+ await f.page.evaluate(() => window.conversationFixture.socket.notify("chat:1", {
1355
+ type: "message", turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer",
1356
+ streaming: { revision: 1, messages: [{ turnId: "000005", messageId: "live-answer", role: "assistant", status: "inProgress", text: "Live answer" }] }
1357
+ }));
1358
+ await expect(f.primary.getByText("Live answer", { exact: true })).toHaveCount(1);
1359
+ await expect(f.primary.getByText("Saved question 2", { exact: true })).toHaveCount(1);
1360
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), true);
1361
+ await expect(f.primary.getByText("Saved question 1", { exact: true })).toHaveCount(1);
1362
+ await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
1363
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().loadMore()), false);
1364
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1365
+ await expect(f.primary.getByText("Saved question 1", { exact: true })).toHaveCount(0);
1366
+ await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toBeVisible();
1367
+ assert.deepEqual(await f.sent(), []);
1368
+ });
1369
+
1370
+
1371
+ test("original product delivery envelopes retain exact admission and retry without manufactured receipts", options, async t => {
1372
+ const f = await fixture(t);
1373
+ const target = async id => {
1374
+ await f.page.evaluate(id => window.conversationFixture.target(id), id);
1375
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current()?.snapshot.value?.id)).toBe(id);
1376
+ await expect(f.input).toBeEnabled();
1377
+ };
1378
+ const send = (messageId, message, data) => f.page.evaluate(({ messageId, message, data }) =>
1379
+ window.conversationFixture.current().send({ message, data }, { messageId }), { messageId, message, data });
1380
+ const message = messageId => f.page.evaluate(messageId => {
1381
+ const value = window.conversationFixture.current().delivery.find(messageId);
1382
+ return value ? { id: value.id, status: value.status, error: value.error } : null;
1383
+ }, messageId);
1384
+
1385
+ await f.command("mode", { mode: "product-accepted" });
1386
+ assert.deepEqual(await send("product-admitted", "A product result is sufficient"), {
1387
+ ok: true, delivered: true, messageId: "product-admitted"
1388
+ });
1389
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().turns.value
1390
+ .filter(turn => turn.user?.messageId === "product-admitted").length)).toBe(1);
1391
+ assert.equal(await message("product-admitted"), null, "Only the actual authored row reconciles the optimistic entry");
1392
+
1393
+ await target("chat:2");
1394
+ await f.command("mode", { mode: "product-rejected" });
1395
+ const captured = { originId: "original-tab", agentSettings: { model: "original-choice" } };
1396
+ assert.deepEqual(await send("product-rejected", "Retry this captured request", captured), {
1397
+ ok: false, delivered: false, messageId: "product-rejected", code: "product_owner_conflict",
1398
+ error: "This turn belongs to another user.", operationOutcome: "active_turn_owned_by_another_user",
1399
+ retryable: true, refreshRecommended: true
1400
+ });
1401
+ assert.equal((await message("product-rejected")).status, "failed");
1402
+ await f.input.fill("Do not clear my newer draft");
1403
+ await f.command("mode", { mode: "product-accepted" });
1404
+ await send("product-rejected", "A later caller must not replace the retry", { originId: "later-tab" });
1405
+ const retries = (await f.sent()).filter(request => request.id === "chat:2");
1406
+ assert.equal(retries.length, 2);
1407
+ assert.deepEqual(retries[1].input, retries[0].input);
1408
+ assert.deepEqual(retries[1].input.data, captured);
1409
+ await expect(f.input).toHaveValue("Do not clear my newer draft");
1410
+
1411
+ await target("chat:3");
1412
+ await f.command("mode", { mode: "product-native-duplicate" });
1413
+ assert.deepEqual(await send("native-history-only", "Already accepted by the original native owner"), {
1414
+ ok: true, delivered: true, messageId: "native-history-only"
1415
+ });
1416
+ assert.equal((await message("native-history-only")).status, "accepted");
1417
+ assert.deepEqual((await f.state()).states["chat:3"].conversationLog, []);
1418
+ const duplicateReads = (await f.state()).requests.filter(request => request.id === "chat:3" && request.method === "GET").length;
1419
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1420
+ await expect.poll(async () => (await f.state()).requests.filter(request => request.id === "chat:3" && request.method === "GET").length).toBeGreaterThan(duplicateReads);
1421
+ assert.equal((await message("native-history-only")).status, "accepted");
1422
+ assert.deepEqual((await f.state()).states["chat:3"].conversationLog, []);
1423
+ assert.equal((await f.sent()).filter(request => request.id === "chat:3").length, 1);
1424
+ assert.equal((await f.state()).requests.filter(request => request.suffix.startsWith("/deliveries/")).length, 0,
1425
+ "A successful product envelope needs no inspect-after-send request");
1426
+
1427
+ await target("chat:4");
1428
+ await f.command("mode", { mode: "uncertain" });
1429
+ assert.equal(await send("lost-product-response", "Keep the exact lost request"), false);
1430
+ assert.equal((await message("lost-product-response")).status, "uncertain");
1431
+ const uncertainReads = (await f.state()).requests.filter(request => request.id === "chat:4" && request.method === "GET").length;
1432
+ await f.page.evaluate(() => window.conversationFixture.socket.reconnect());
1433
+ await expect.poll(async () => (await f.state()).requests.filter(request => request.id === "chat:4" && request.method === "GET").length).toBeGreaterThan(uncertainReads);
1434
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.current().inspectDelivery("lost-product-response")), {
1435
+ status: "unknown", messageId: "lost-product-response"
1436
+ });
1437
+ assert.equal((await message("lost-product-response")).status, "uncertain");
1438
+ assert.equal((await f.sent()).filter(request => request.id === "chat:4").length, 1);
1439
+ assert.deepEqual((await f.state()).states["chat:4"].conversationLog, []);
1440
+
1441
+ await target("chat:5");
1442
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().send({
1443
+ message: "Captured steering", request: { text: "Captured steering", steer: true }
1444
+ }, { messageId: "unsupported-steer" })), false);
1445
+ assert.equal((await f.sent()).filter(request => request.id === "chat:5").length, 0,
1446
+ "An explicit captured request cannot invent steering capability");
1447
+ });
1448
+
1449
+ test("dynamic acquisition keeps the original shared readers and releases only its setup-owned scopes", options, async t => {
1450
+ const f = await fixture(t);
1451
+ await f.page.evaluate(() => {
1452
+ const acquire = window.conversationFixture.acquireConversation;
1453
+ window.dynamicFirst = acquire({ conversationId: "chat:2" });
1454
+ window.dynamicMirror = acquire({ conversationId: "chat:2" });
1455
+ window.dynamicOther = acquire({ conversationId: "chat:3" });
1456
+ window.dynamicFirst.runtime.value.draft.value = "One retained task draft.";
1457
+ });
1458
+ await expect.poll(() => f.page.evaluate(() => window.dynamicFirst.runtime.value.available.value &&
1459
+ window.dynamicOther.runtime.value.available.value)).toBe(true);
1460
+ assert.equal(await f.page.evaluate(() => window.dynamicFirst.runtime.value === window.dynamicMirror.runtime.value), true);
1461
+ assert.equal(await f.page.evaluate(() => window.dynamicMirror.runtime.value.draft.value), "One retained task draft.");
1462
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).active, 3);
1463
+ await f.page.evaluate(() => window.conversationFixture.socket.notify("chat:2", {
1464
+ type: "message", messageId: "dynamic-answer", role: "assistant", status: "inProgress", text: "Same stream.",
1465
+ streaming: { revision: 1, messages: [{ messageId: "dynamic-answer", role: "assistant", status: "inProgress", text: "Same stream." }] }
1466
+ }));
1467
+ assert.equal(await f.page.evaluate(() => window.dynamicMirror.runtime.value.turns.value[0].assistant.text), "Same stream.");
1468
+ await f.page.evaluate(() => { window.dynamicFirst.release(); window.dynamicFirst.release(); });
1469
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).active, 3);
1470
+ assert.equal(await f.page.evaluate(() => window.dynamicMirror.runtime.value.draft.value), "One retained task draft.");
1471
+ await f.page.evaluate(() => window.dynamicOther.release());
1472
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).active, 2);
1473
+ await f.page.evaluate(() => window.conversationFixture.releaseFactory());
1474
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.socket.inspect())).active, 1);
1475
+ assert.equal(await f.page.evaluate(() => {
1476
+ try { window.conversationFixture.acquireConversation({ conversationId: "chat:4" }); return false; }
1477
+ catch (failure) { return /scope has ended/.test(failure.message); }
1478
+ }), true);
1479
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 0);
1480
+ assert.equal((await f.sent()).length, 0);
1481
+ });
1482
+
1483
+ test("prepared submission keeps optimism before application creation and uses the same captured receipt owner", options, async t => {
1484
+ const f = await fixture(t);
1485
+ await f.command("mode", { mode: "hold" });
1486
+ await f.page.evaluate(() => {
1487
+ window.preparedBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:2", active: false });
1488
+ window.prepareSave = Promise.withResolvers();
1489
+ window.prepareCreate = Promise.withResolvers();
1490
+ window.prepareOrder = [];
1491
+ window.preparedAcceptance = 0;
1492
+ const payload = { message: "Original native prompt.", displayMessage: "Original displayed prompt.",
1493
+ displayAttachments: [{ attachmentId: "original-file" }], request: {
1494
+ text: "Original native prompt.", attachmentIds: ["original-file"],
1495
+ data: { agentSettings: { model: "original-model" }, presentation: { draft: "" } }
1496
+ } };
1497
+ window.preparedBinding.runtime.value.draft.value = "Newer composer draft.";
1498
+ window.preparedResult = window.preparedBinding.runtime.value.submitPrepared(payload, {
1499
+ messageId: "prepared-message", onAccepted() { window.preparedAcceptance += 1; },
1500
+ async prepare(captured, { signal }) {
1501
+ window.preparedSignal = signal;
1502
+ window.prepareOrder.push("save");
1503
+ await window.prepareSave.promise;
1504
+ window.prepareOrder.push("create");
1505
+ await window.prepareCreate.promise;
1506
+ window.prepareOrder.push("dispatch");
1507
+ return { ...captured.request, data: { ...captured.request.data,
1508
+ presentation: { ...captured.request.data.presentation, draft: window.preparedBinding.runtime.value.draft.value } } };
1509
+ }
1510
+ });
1511
+ payload.message = "Changed after Send.";
1512
+ payload.request.text = "Changed after Send.";
1513
+ payload.request.attachmentIds.push("later-file");
1514
+ payload.request.data.agentSettings.model = "later-model";
1515
+ });
1516
+ const pending = await f.page.evaluate(() => window.preparedBinding.runtime.value.delivery.state.messages);
1517
+ assert.equal(pending.length, 1);
1518
+ assert.equal(pending[0].id, "prepared-message");
1519
+ assert.equal(pending[0].status, "pending");
1520
+ assert.equal(pending[0].text, "Original displayed prompt.");
1521
+ assert.deepEqual(await f.page.evaluate(() => window.prepareOrder), ["save"]);
1522
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value.snapshot.value), null);
1523
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value.send({ message: "Ordinary Send still waits." })), false);
1524
+ assert.equal((await f.state()).requests.filter(request => request.id === "chat:2").length, 0);
1525
+ await f.page.evaluate(() => window.prepareSave.resolve());
1526
+ await expect.poll(() => f.page.evaluate(() => window.prepareOrder)).toEqual(["save", "create"]);
1527
+ assert.equal((await f.sent()).length, 0);
1528
+ await f.page.evaluate(() => window.prepareCreate.resolve());
1529
+ await expect.poll(async () => (await f.state()).held).toBe(1);
1530
+ const [request] = await f.sent();
1531
+ assert.equal(request.id, "chat:2");
1532
+ assert.deepEqual(request.input, { messageId: "prepared-message", text: "Original native prompt.",
1533
+ attachmentIds: ["original-file"], data: { agentSettings: { model: "original-model" },
1534
+ presentation: { draft: "Newer composer draft." } } });
1535
+ await f.page.evaluate(() => {
1536
+ window.preparedReader = window.conversationFixture.acquireConversation({ conversationId: "chat:2" });
1537
+ });
1538
+ await expect.poll(() => f.page.evaluate(() => window.preparedReader.runtime.value.available.value)).toBe(true);
1539
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value === window.preparedReader.runtime.value), true);
1540
+ await f.command("accept", { holdResponse: true });
1541
+ await f.notify("chat:2");
1542
+ const result = await f.page.evaluate(() => window.preparedResult);
1543
+ assert.equal(result.ok, true, "The exact authored row settles before the held HTTP response");
1544
+ assert.equal(await f.page.evaluate(() => window.preparedAcceptance), 1);
1545
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value.delivery.state.messages.length), 0);
1546
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value.turns.value.length), 1);
1547
+ assert.equal(await f.page.evaluate(() => window.preparedBinding.runtime.value.draft.value), "Newer composer draft.");
1548
+ assert.equal((await f.state()).held, 1);
1549
+ await f.command("release-response");
1550
+ assert.equal((await f.sent()).length, 1);
1551
+ });
1552
+
1553
+ test("prepared failure is retryable before dispatch and remains inspection-only after an unknown receipt", options, async t => {
1554
+ const f = await fixture(t);
1555
+ await f.page.evaluate(() => {
1556
+ window.failedBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:3", active: false });
1557
+ });
1558
+ assert.equal(await f.page.evaluate(() => {
1559
+ try { window.failedBinding.runtime.value.submitPrepared({ message: "Invalid preparation.", request: { text: "Invalid preparation." } }); return false; }
1560
+ catch (failure) { return /requires application preparation/.test(failure.message); }
1561
+ }), true);
1562
+ assert.deepEqual(await f.page.evaluate(() => window.failedBinding.runtime.value.delivery.state.messages), []);
1563
+ const rejected = await f.page.evaluate(() => window.failedBinding.runtime.value.submitPrepared({
1564
+ message: "Captured task.", request: { text: "Captured task.", data: { selection: "original" } }
1565
+ }, { messageId: "prepared-retry", prepare() { throw Object.assign(new Error("Create was rejected."), { code: "application_create_rejected" }); } }));
1566
+ assert.deepEqual(rejected, { ok: false, error: "Create was rejected.", code: "application_create_rejected" });
1567
+ assert.equal(await f.page.evaluate(() => window.failedBinding.runtime.value.delivery.find("prepared-retry").status), "failed");
1568
+ assert.equal((await f.sent()).length, 0);
1569
+ await f.command("mode", { mode: "uncertain" });
1570
+ await f.page.route("**/conversations/*/messages", async route => {
1571
+ const response = await route.fetch();
1572
+ return route.fulfill({ response, status: 404, json: { ...await response.json(), code: "application_record_closed" } });
1573
+ }, { times: 1 });
1574
+ assert.deepEqual(await f.page.evaluate(() => window.failedBinding.runtime.value.submitPrepared({
1575
+ message: "Do not recapture.", request: { text: "Do not recapture.", data: { selection: "changed" } }
1576
+ }, { messageId: "prepared-retry", prepare: captured => captured.request })), {
1577
+ ok: false, status: "uncertain", error: "Connection closed before the receipt arrived.", code: "application_record_closed"
1578
+ });
1579
+ const [request] = await f.sent();
1580
+ assert.deepEqual(request.input, { messageId: "prepared-retry", text: "Captured task.", data: { selection: "original" } });
1581
+ assert.equal(await f.page.evaluate(() => window.failedBinding.runtime.value.delivery.find("prepared-retry").status), "uncertain");
1582
+ assert.equal(await f.page.evaluate(() => window.failedBinding.runtime.value.submitPrepared({
1583
+ message: "No implicit retry.", request: { text: "No implicit retry." }
1584
+ }, { messageId: "prepared-retry", prepare() { throw new Error("An uncertain request must not prepare again."); } })), false);
1585
+ await f.page.evaluate(() => { window.failedReader = window.conversationFixture.acquireConversation({ conversationId: "chat:3" }); });
1586
+ await expect.poll(() => f.page.evaluate(() => window.failedReader.runtime.value.available.value)).toBe(true);
1587
+ assert.deepEqual(await f.page.evaluate(() => window.failedReader.runtime.value.inspectDelivery("prepared-retry")), { status: "unknown", messageId: "prepared-retry" });
1588
+ assert.equal((await f.sent()).length, 1);
1589
+ await f.command("confirm", { id: "chat:3" });
1590
+ const receipt = await f.page.evaluate(() => window.failedReader.runtime.value.inspectDelivery("prepared-retry"));
1591
+ assert.equal(receipt.status, "accepted");
1592
+ assert.equal(receipt.messageId, "prepared-retry");
1593
+ assert.equal((await f.sent()).length, 1);
1594
+
1595
+ await f.command("deny", { denied: true });
1596
+ await f.page.evaluate(() => {
1597
+ window.deniedPreparation = Promise.withResolvers();
1598
+ window.deniedBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:4" });
1599
+ window.deniedSend = window.deniedBinding.runtime.value.submitPrepared({ message: "Access changed.", request: { text: "Access changed." } }, {
1600
+ messageId: "denied-preparation", async prepare(captured) { await window.deniedPreparation.promise; return captured.request; }
1601
+ });
1602
+ });
1603
+ await expect.poll(() => f.page.evaluate(() => window.deniedBinding.error.value)).toBe("Access denied.");
1604
+ await f.page.evaluate(() => window.deniedPreparation.resolve());
1605
+ assert.equal(await f.page.evaluate(() => window.deniedSend), false);
1606
+ assert.equal((await f.sent()).length, 1, "Known access denial during preparation prevents canonical dispatch");
1607
+ await f.page.evaluate(() => window.deniedBinding.release());
1608
+ await f.command("deny", { denied: false });
1609
+ await f.page.evaluate(() => {
1610
+ window.retiredPreparation = Promise.withResolvers();
1611
+ window.releasedPreparation = Promise.withResolvers();
1612
+ window.retiredBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:4", active: false });
1613
+ window.releasedBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:5", active: false });
1614
+ window.retiredSend = window.retiredBinding.runtime.value.submitPrepared({ message: "Retired actor.", request: { text: "Retired actor." } }, {
1615
+ messageId: "retired-preparation", async prepare(captured, { signal }) { window.retiredSignal = signal; await window.retiredPreparation.promise; return captured.request; }
1616
+ });
1617
+ window.releasedSend = window.releasedBinding.runtime.value.submitPrepared({ message: "Closed task.", request: { text: "Closed task." } }, {
1618
+ messageId: "released-preparation", async prepare(captured, { signal }) { window.releasedSignal = signal; await window.releasedPreparation.promise; return captured.request; }
1619
+ });
1620
+ window.releasedBinding.release();
1621
+ window.conversationFixture.actor("43");
1622
+ window.retiredPreparation.resolve();
1623
+ window.releasedPreparation.resolve();
1624
+ });
1625
+ assert.deepEqual(await f.page.evaluate(() => Promise.all([window.retiredSend, window.releasedSend])), [false, false]);
1626
+ assert.deepEqual(await f.page.evaluate(() => [window.retiredSignal.aborted, window.releasedSignal.aborted]), [true, true]);
1627
+ assert.equal((await f.sent()).length, 1, "Neither a retired actor nor a released task dispatches after preparation");
1628
+ });
1629
+
1630
+ test("saved prepared conversations can Stop before the first read while inactive and denied targets stay closed", options, async t => {
1631
+ const f = await fixture(t);
1632
+ const readsReleased = Promise.withResolvers();
1633
+ const heldReads = new Set();
1634
+ await f.page.route(/\/api\/assistant\/home\/conversations\/chat%3A[23](?:\?.*)?$/u, async route => {
1635
+ heldReads.add(decodeURIComponent(new URL(route.request().url()).pathname.split("/").at(-1)));
1636
+ await readsReleased.promise;
1637
+ await route.continue();
1638
+ });
1639
+ try {
1640
+ await f.page.evaluate(() => {
1641
+ window.unsavedTask = window.conversationFixture.acquireConversation({ conversationId: "chat:2", active: false });
1642
+ window.unidentifiedTask = window.conversationFixture.acquireConversation({ conversationId: "" });
1643
+ });
1644
+ assert.equal(await f.page.evaluate(() => window.unsavedTask.runtime.value.cancel()), false);
1645
+ assert.equal(await f.page.evaluate(() => window.unidentifiedTask.runtime.value), null);
1646
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 0);
1647
+ const accepted = await f.page.evaluate(() => window.unsavedTask.runtime.value.submitPrepared({
1648
+ message: "Accepted before the first read.", request: { text: "Accepted before the first read." }
1649
+ }, { messageId: "accepted-before-read", prepare(captured) {
1650
+ window.savedTask = window.conversationFixture.acquireConversation({ conversationId: "chat:2" });
1651
+ return captured.request;
1652
+ } }));
1653
+ assert.equal(accepted.status, "accepted");
1654
+ await expect.poll(() => heldReads.has("chat:2")).toBe(true);
1655
+ assert.deepEqual(await f.page.evaluate(() => ({
1656
+ snapshot: window.savedTask.runtime.value.snapshot.value,
1657
+ available: window.savedTask.runtime.value.available.value,
1658
+ editable: window.savedTask.runtime.value.editable.value,
1659
+ sending: window.savedTask.runtime.value.delivery.state.sending
1660
+ })), { snapshot: null, available: false, editable: true, sending: false });
1661
+ assert.deepEqual(await f.page.evaluate(() => window.savedTask.runtime.value.cancel()), { stopped: true });
1662
+
1663
+ await f.command("mode", { mode: "hold" });
1664
+ await f.page.evaluate(() => {
1665
+ window.pendingTask = window.conversationFixture.acquireConversation({ conversationId: "chat:3", active: false });
1666
+ window.pendingStart = window.pendingTask.runtime.value.submitPrepared({
1667
+ message: "Keep Stop independent of the Start response.", request: { text: "Keep Stop independent of the Start response." }
1668
+ }, { messageId: "held-start-before-read", prepare(captured) {
1669
+ window.pendingReader = window.conversationFixture.acquireConversation({ conversationId: "chat:3" });
1670
+ return captured.request;
1671
+ } });
1672
+ });
1673
+ await expect.poll(async () => (await f.state()).held).toBe(1);
1674
+ await expect.poll(() => heldReads.has("chat:3")).toBe(true);
1675
+ await f.command("accept", { holdResponse: true });
1676
+ assert.equal(await f.page.evaluate(() => window.pendingTask.runtime.value.snapshot.value), null);
1677
+ assert.equal(await f.page.evaluate(() => window.pendingTask.runtime.value.delivery.state.sending), true);
1678
+ assert.deepEqual(await f.page.evaluate(() => window.pendingTask.runtime.value.cancel()), { stopped: true });
1679
+ assert.equal((await f.state()).held, 1, "Stop does not await or replay the pending Start response");
1680
+ await f.command("release-response");
1681
+ assert.equal((await f.page.evaluate(() => window.pendingStart)).status, "accepted");
1682
+
1683
+ await f.command("deny", { denied: true });
1684
+ await f.page.evaluate(() => {
1685
+ window.deniedStop = window.conversationFixture.acquireConversation({ conversationId: "chat:4" });
1686
+ });
1687
+ await expect.poll(() => f.page.evaluate(() => window.deniedStop.error.value)).toBe("Access denied.");
1688
+ assert.equal(await f.page.evaluate(() => window.deniedStop.runtime.value.cancel()), false);
1689
+ await f.command("deny", { denied: false });
1690
+ readsReleased.resolve();
1691
+ await expect.poll(() => f.page.evaluate(() => window.savedTask.runtime.value.available.value &&
1692
+ window.pendingTask.runtime.value.available.value)).toBe(true);
1693
+ assert.deepEqual((await f.state()).requests.filter(request => request.suffix === "/cancel").map(request => request.id), ["chat:2", "chat:3"]);
1694
+ assert.deepEqual((await f.sent()).map(request => request.input.messageId), ["accepted-before-read", "held-start-before-read"]);
1695
+ assert.deepEqual(await f.page.evaluate(() => [window.savedTask.runtime.value.snapshot.value.status,
1696
+ window.pendingTask.runtime.value.snapshot.value.status]), ["ready", "ready"]);
1697
+ await f.page.evaluate(() => {
1698
+ window.retiredStop = window.savedTask.runtime.value;
1699
+ window.conversationFixture.actor("43");
1700
+ });
1701
+ assert.equal(await f.page.evaluate(() => window.retiredStop.cancel()), false);
1702
+ assert.equal((await f.state()).requests.filter(request => request.suffix === "/cancel").length, 2);
1703
+ assert.equal((await f.sent()).length, 2);
1704
+ } finally {
1705
+ readsReleased.resolve();
1706
+ }
1707
+ });
1708
+
1709
+ test("application defaults configure standard cards once while preserving overrides and actor isolation", options, async t => {
1710
+ const f = await fixture(t, 1280, { applicationDefaults: true });
1711
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().identity.actorKey), "application-user");
1712
+ await f.input.fill("This user's private draft");
1713
+ await f.page.evaluate(() => { window.conversationFixture.retainVoice(); window.conversationFixture.mirror(true); });
1714
+ await expect(f.page.locator("[data-mirror]").getByRole("textbox", { name: "Message AI assistant" })).toHaveValue("This user's private draft");
1715
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.socket.inspect().active), 1);
1716
+
1717
+ await f.page.evaluate(() => window.conversationDefaultsFixture.mountOtherApp());
1718
+ await expect.poll(() => f.page.evaluate(() => window.conversationDefaultsFixture.other()?.available.value)).toBe(true);
1719
+ assert.deepEqual(await f.page.evaluate(() => {
1720
+ const other = window.conversationDefaultsFixture.other();
1721
+ const initial = [other.identity.actorKey, other.draft.value];
1722
+ other.draft.value = "The other application's draft";
1723
+ return initial;
1724
+ }), ["42", ""], "Another Vue application retains its own placement identity and draft");
1725
+ await expect(f.input).toHaveValue("This user's private draft");
1726
+
1727
+ await f.page.evaluate(() => {
1728
+ window.overrideConversation = window.conversationFixture.acquireConversation({
1729
+ conversationId: "chat:2", actorKey: "override-user", api: window.conversationDefaultsFixture.overrideApi
1730
+ });
1731
+ window.emptyActorConversation = window.conversationFixture.acquireConversation({ conversationId: "chat:3", actorKey: "" });
1732
+ });
1733
+ await expect.poll(() => f.page.evaluate(() => window.overrideConversation.runtime.value?.available.value)).toBe(true);
1734
+ assert.equal(await f.page.evaluate(() => window.overrideConversation.runtime.value.identity.actorKey), "override-user");
1735
+ assert.equal(await f.page.evaluate(() => window.emptyActorConversation.runtime.value), null,
1736
+ "An explicit empty actor cannot inherit an earlier application's subject");
1737
+ await f.page.evaluate(() => window.overrideConversation.runtime.value.reload());
1738
+ assert.ok(await f.page.evaluate(() => window.conversationDefaultsFixture.overrideRequests.some(path => path.includes("chat%3A2"))));
1739
+ assert.equal(await f.page.evaluate(() => window.conversationDefaultsFixture.requests.some(path => path.includes("chat%3A2"))), false);
1740
+ await f.page.evaluate(() => { window.overrideConversation.release(); window.emptyActorConversation.release(); });
1741
+
1742
+ await f.command("mode", { mode: "hold" });
1743
+ await f.input.press("Enter");
1744
+ await expect.poll(async () => (await f.sent()).length).toBe(1);
1745
+ await expect(f.input).toHaveValue("This user's private draft");
1746
+ assert.ok(await f.page.evaluate(() => window.conversationDefaultsFixture.requests.some(path => path.endsWith("/messages"))));
1747
+ await f.command("accept");
1748
+ await expect(f.input).toHaveValue("");
1749
+ await f.command("finish", { id: "chat:1", text: "Accepted with application defaults." });
1750
+ await f.notify("chat:1");
1751
+ await f.input.fill("Do not retain this after logout");
1752
+ await f.page.evaluate(() => window.conversationDefaultsFixture.actor(""));
1753
+ await expect(f.input).toBeDisabled();
1754
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.voiceState().current)).toBe(false);
1755
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.sendVoice("Retired application actor")), false);
1756
+ assert.equal((await f.page.evaluate(() => window.conversationFixture.voiceState())).draft, "");
1757
+ assert.deepEqual(await f.page.evaluate(() => {
1758
+ const other = window.conversationDefaultsFixture.other();
1759
+ return [other.current.value, other.identity.actorKey, other.draft.value];
1760
+ }), [true, "42", "The other application's draft"]);
1761
+ await f.page.evaluate(() => window.conversationDefaultsFixture.actor("next-user"));
1762
+ await expect(f.input).toBeEnabled();
1763
+ await expect(f.input).toHaveValue("");
1764
+ assert.equal(await f.page.evaluate(() => window.conversationFixture.current().identity.actorKey), "next-user");
1765
+ await f.page.evaluate(() => { window.conversationDefaultsFixture.unmountOtherApp(); window.conversationFixture.releaseVoice(); });
1766
+ assert.equal((await f.sent()).length, 1, "Changing an application identity never replays its earlier submission");
1767
+ });
1768
+
1769
+ // Ported composer assertions from Public Vibe64's failed Temporary retry and
1770
+ // restored-unconfirmed cases. The original application cases remain unchanged.
1771
+ test("prepared binding preserves retry identity, newer composer input and inspection-only uncertainty", options, async t => {
1772
+ const f = await fixture(t);
1773
+ const rejected = await f.page.evaluate(async () => {
1774
+ window.taskBinding = window.conversationFixture.acquireConversation({ conversationId: "chat:3", active: false });
1775
+ const current = window.taskBinding.runtime.value;
1776
+ current.draft.value = "Repair this conflict.";
1777
+ current.draftAttachments.value = [{ attachmentId: "original-file", fileName: "first.txt" }];
1778
+ window.taskClearedFiles = [];
1779
+ window.taskAccepted = 0;
1780
+ return window.taskBinding.submitPrepared({ message: current.draft.value,
1781
+ displayAttachments: current.draftAttachments.value, data: { model: "original-model" } }, {
1782
+ messageId: "task-retry", working: true,
1783
+ attachments: { clearAttachments({ attachmentIds }) { window.taskClearedFiles.push(attachmentIds); } },
1784
+ onAccepted() { window.taskAccepted += 1; },
1785
+ prepare() { throw new Error("Create was rejected."); }
1786
+ });
1787
+ });
1788
+ assert.deepEqual(rejected, { ok: false, error: "Create was rejected." });
1789
+ assert.equal(await f.page.evaluate(() => window.taskBinding.runtime.value.draft.value), "");
1790
+ assert.equal(await f.page.evaluate(() => window.taskBinding.runtime.value.delivery.find("task-retry").status), "failed");
1791
+ assert.equal((await f.sent()).length, 0);
1792
+ await f.page.evaluate(() => {
1793
+ const current = window.taskBinding.runtime.value;
1794
+ current.draft.value = "A newer question.";
1795
+ current.draftAttachments.value = [
1796
+ { attachmentId: "original-file", fileName: "first.txt" },
1797
+ { attachmentId: "new-file", fileName: "next.txt" }
1798
+ ];
1799
+ window.taskReader = window.conversationFixture.acquireConversation({ conversationId: "chat:3" });
1800
+ });
1801
+ await expect.poll(() => f.page.evaluate(() => window.taskReader.runtime.value.available.value)).toBe(true);
1802
+ await f.command("mode", { mode: "uncertain" });
1803
+ const uncertain = await f.page.evaluate(() => window.taskBinding.submitPrepared({
1804
+ message: "Do not recapture.", data: { model: "another-model" }
1805
+ }, { retryMessageId: "task-retry", working: false, prepare: captured => captured.request }));
1806
+ assert.equal(uncertain.status, "uncertain");
1807
+ const [request] = await f.sent();
1808
+ assert.deepEqual(request.input, { messageId: "task-retry", text: "Repair this conflict.",
1809
+ attachmentIds: ["original-file"], data: { model: "original-model" }, steer: true });
1810
+ const checked = await f.page.evaluate(() => window.taskBinding.submitPrepared({ message: "Never replay." }, {
1811
+ retryMessageId: "task-retry", prepare() { throw new Error("Inspection must not prepare or resend."); }
1812
+ }));
1813
+ assert.equal(checked.status, "uncertain");
1814
+ assert.equal((await f.sent()).length, 1);
1815
+ assert.equal(await f.page.evaluate(() => window.taskBinding.runtime.value.draft.value), "A newer question.");
1816
+ assert.deepEqual(await f.page.evaluate(() => window.taskClearedFiles), []);
1817
+ await f.command("confirm", { id: "chat:3" });
1818
+ const receipt = await f.page.evaluate(() => window.taskBinding.submitPrepared({ message: "Never replay." }, {
1819
+ retryMessageId: "task-retry", prepare() { throw new Error("Inspection must not prepare or resend."); }
1820
+ }));
1821
+ assert.equal(receipt.ok, true);
1822
+ assert.equal(receipt.status, "accepted");
1823
+ assert.equal((await f.sent()).length, 1);
1824
+ assert.equal(await f.page.evaluate(() => window.taskAccepted), 1);
1825
+ assert.equal(await f.page.evaluate(() => window.taskBinding.runtime.value.draft.value), "A newer question.");
1826
+ assert.deepEqual(await f.page.evaluate(() => window.taskBinding.runtime.value.draftAttachments.value.map(file => file.attachmentId)), ["new-file"]);
1827
+ assert.deepEqual(await f.page.evaluate(() => window.taskClearedFiles), [["original-file"]]);
1828
+ });
1829
+
1830
+ test("acceptance settles a voice-retained conversation after its text view retargets without clearing the next composer", options, async t => {
1831
+ const f = await fixture(t, 1280, { acceptedDraft: true });
1832
+ await f.page.evaluate(() => window.conversationFixture.attachments(true));
1833
+ await f.primary.locator("input[type=file]").setInputFiles({ name: "first.txt", mimeType: "text/plain", buffer: Buffer.from("Original file") });
1834
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.attachmentState().ready.length)).toBe(1);
1835
+ const [originalFile] = (await f.page.evaluate(() => window.conversationFixture.attachmentState())).ready;
1836
+ await f.page.evaluate(() => {
1837
+ window.originalConversation = window.conversationFixture.current();
1838
+ window.originalConversation.draftAttachments.value = window.conversationFixture.attachmentState().ready;
1839
+ window.conversationFixture.retainVoice();
1840
+ });
1841
+ await f.command("mode", { mode: "hold" });
1842
+ await f.input.fill("Accept the original conversation's draft.");
1843
+ await f.input.press("Enter");
1844
+ await expect.poll(async () => (await f.state()).held).toBe(1);
1845
+ const [sent] = await f.sent();
1846
+ assert.deepEqual(sent.input.attachmentIds, [originalFile.attachmentId]);
1847
+ await f.page.evaluate(() => window.conversationFixture.target("chat:2"));
1848
+ await expect(f.input).toBeEnabled();
1849
+ await f.input.fill("The next conversation's draft stays.");
1850
+ await f.primary.locator("input[type=file]").setInputFiles({ name: "next.txt", mimeType: "text/plain", buffer: Buffer.from("Newer file") });
1851
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.attachmentState().ready.length)).toBe(1);
1852
+ const [nextFile] = (await f.page.evaluate(() => window.conversationFixture.attachmentState())).ready;
1853
+ await f.page.evaluate(() => {
1854
+ window.conversationFixture.current().draftAttachments.value = window.conversationFixture.attachmentState().ready;
1855
+ });
1856
+ await f.command("accept", { holdResponse: true });
1857
+ await f.notify("chat:1");
1858
+ await expect.poll(() => f.page.evaluate(() => window.originalConversation.draft.value)).toBe("");
1859
+ assert.equal(await f.page.evaluate(() => window.originalConversation.draftMessageId.value), "");
1860
+ assert.deepEqual(await f.page.evaluate(() => window.originalConversation.draftAttachments.value), []);
1861
+ await expect(f.input).toHaveValue("The next conversation's draft stays.");
1862
+ assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.current().draftAttachments.value.map(file => file.attachmentId)), [nextFile.attachmentId]);
1863
+ assert.deepEqual((await f.page.evaluate(() => window.conversationFixture.attachmentState())).ready.map(file => file.attachmentId), [nextFile.attachmentId]);
1864
+ assert.deepEqual((await f.page.evaluate(() => window.conversationFixture.attachmentState())).acknowledged, []);
1865
+ await f.command("release-response");
1866
+ await expect(f.input).toHaveValue("The next conversation's draft stays.");
1867
+ assert.equal((await f.sent()).length, 1);
1868
+ });