返回 DeepSeek-Reasonix
composer-queue-integration.test.tsx
根目录 / desktop / frontend / src / __tests__ / composer-queue-integration.test.tsx
1 import assert from "node:assert/strict";
2 import { act } from "react";
3 import { installDom, installBridgeApp, renderComposer } from "./composerInboxHarness";
4 import { createInboxQueuePreviewBindings } from "../lib/inboxQueuePreview";
5 import type { InboxQueueRequest } from "../lib/inboxQueueCommands";
6 import type { InboxTarget } from "../lib/pendingFollowup";
7
8 const dom = installDom();
9 globalThis.sessionStorage = dom.window.sessionStorage;
10 const fixture = createInboxQueuePreviewBindings();
11 const calls: InboxQueueRequest[] = [];
12 let enqueues = 0;
13 let cancelled: string[] | undefined;
14 let otherHost = false;
15 let failSave = false;
16 let departOnSave = false;
17 installBridgeApp({
18 ...fixture,
19 CaptureAttachmentTarget: async () => ({ token: "queue-test-attachment", capabilities: ["attachments-v2"] }),
20 ReleaseAttachmentTarget: async () => {},
21 StageImageForTarget: async () => ({ draftId: "0123456789abcdef0123456789abcdef", displayName: "draft.png", mime: "image/png", width: 1, height: 1, bytes: 12 }),
22 ReadDraftImageForTarget: async () => "data:image/png;base64,iVBORw0KGgo=",
23 SavePastedImageForTab: async () => ".reasonix/attachments/draft.png",
24 AttachmentDataURLForTab: async () => "data:image/png;base64,iVBORw0KGgo=",
25 InboxSnapshot: async () => otherHost ? { revision: 1, sessionPath: "session-a", mutationsSupported: true, items: [{ id: "other-host", preview: "other host queue", state: "queued" }] } : fixture.InboxSnapshot("tab-a"),
26 ListTabs: async () => [{ id: "tab-a", turnId: "live-turn" }],
27 InboxQueueForTarget: async (target: InboxTarget, request: InboxQueueRequest) => {
28 calls.push(request);
29 if (request.kind === "edit" && failSave) return { outcome: "conflict", reason: "content_changed" };
30 if (request.kind === "edit" && departOnSave) {
31 await fixture.InboxQueueForTarget!(target, { kind: "delete", itemId: request.itemId });
32 }
33 return fixture.InboxQueueForTarget!(target, request);
34 },
35 EnqueueInboxFollowupForTarget: async (...args: Parameters<NonNullable<typeof fixture.EnqueueInboxFollowupForTarget>>) => {
36 enqueues++;
37 return fixture.EnqueueInboxFollowupForTarget!(...args);
38 },
39 });
40 const flush = () => new Promise<void>(resolve => setTimeout(resolve, 20));
41 const view = await renderComposer({ running: true, onCancel: async ids => { cancelled = ids; return { discardedItemIds: [] }; } });
42 await act(async () => { await import("../components/ComposerInboxQueue"); await flush(); });
43 await view.rerender({ insertRequest: { id: 8001, text: "default next turn", mode: "replace" } });
44 await act(async () => { (document.querySelector(".composer__btn--send") as HTMLButtonElement).click(); await flush(); });
45 assert.equal(enqueues, 1, "ordinary send while running enqueues a follow-up");
46 assert.ok(!calls.some(call => call.kind === "enqueue_steer"));
47 await view.rerender({ insertRequest: { id: 8002, text: "explicit current turn", mode: "replace" } });
48 await act(async () => { (document.querySelector(".composer__queue-steer") as HTMLButtonElement).click(); await flush(); });
49 assert.equal(enqueues, 1);
50 assert.equal(calls.at(-1)?.kind, "enqueue_steer");
51 assert.equal(calls.at(-1)?.turnId, "live-turn");
52 assert.ok(calls.at(-1)?.idempotencyKey);
53
54 await view.rerender({ insertRequest: { id: 8003, text: "main draft survives editing", mode: "replace" } });
55 const paste = new Event("paste", { bubbles: true, cancelable: true });
56 Object.defineProperty(paste, "clipboardData", { value: { files: [new File(["fixture"], "draft.png", { type: "image/png" })], items: [], types: [], getData: () => "" } });
57 await act(async () => { document.querySelector("textarea.composer__input")!.dispatchEvent(paste); await flush(); });
58 const attachmentCard = document.querySelector(".composer-context__item");
59 assert.ok(attachmentCard, "main composer has an attachment before editing");
60 const edit = document.querySelector('.inbox-queue__row[data-item-id="queue-save"] button[aria-label="Edit"]') as HTMLButtonElement;
61 assert.ok(edit);
62 await act(async () => { (document.querySelector('.inbox-queue__row[data-item-id="queue-save"] button[aria-label="Edit"]') as HTMLButtonElement).click(); await flush(); });
63 const textarea = document.querySelector(".inbox-queue__editor textarea") as HTMLTextAreaElement;
64 assert.ok(textarea.value.includes("完成后验证"), "the editor receives the complete body");
65 assert.equal(textarea.closest("li")?.dataset.itemId, "queue-save", "editor belongs to the original row");
66 assert.equal(document.querySelectorAll(".inbox-queue__editor").length, 1);
67 assert.equal(document.querySelectorAll(".inbox-queue__row").length, 2, "editing does not expand unrelated rows");
68 assert.equal(textarea.closest("li")?.querySelector('.inbox-queue__body'), null, "no duplicate message preview while editing");
69 assert.equal(textarea.closest("li")?.querySelectorAll("footer button").length, 2, "only cancel and save in ordinary editing");
70 assert.equal(document.querySelector(".inbox-queue__running")?.closest(".inbox-queue__editor"), null);
71 assert.ok(document.querySelector(".inbox-queue__running button"), "stop stays available outside editor");
72 assert.equal(document.querySelector(".composer-wrap")?.getAttribute("data-queue-editing"), "true");
73 assert.equal(document.querySelector(".composer-context__item"), attachmentCard, "hiding preserves the mounted attachment");
74 await act(async () => { textarea.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", metaKey: true, bubbles: true })); await flush(); });
75 assert.equal(calls.at(-1)?.kind, "edit");
76 assert.equal((document.querySelector("textarea.composer__input") as HTMLTextAreaElement).value, "main draft survives editing");
77 assert.equal(enqueues, 1, "saving an edit never adds a new message");
78 assert.equal(document.querySelector(".composer-context__item"), attachmentCard, "saved queue edits do not clear main attachments");
79 assert.equal(document.querySelector(".composer-wrap")?.getAttribute("data-queue-editing"), null);
80 await act(async () => { (document.querySelector('.inbox-queue__row[data-item-id="queue-save"] button[aria-label="Edit"]') as HTMLButtonElement).click(); await flush(); });
81 assert.ok(document.querySelector(".inbox-queue__editor"));
82 await act(async () => {
83 document.querySelector(".inbox-queue__editor textarea")!.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
84 await flush();
85 });
86 assert.equal(document.querySelector(".inbox-queue__editor"), null);
87 assert.equal(document.querySelector(".composer-wrap")?.getAttribute("data-queue-editing"), null);
88 assert.ok(!document.querySelector(".inbox-queue")!.textContent!.includes("Your edits have been kept"), "unchanged cancellation leaves no draft banner");
89 assert.equal(cancelled, undefined, "Escape exits editing without stopping the task");
90 await act(async () => { (document.querySelector('.inbox-queue__row[data-item-id="queue-save"] button[aria-label="Edit"]') as HTMLButtonElement).click(); await flush(); });
91 await act(async () => {
92 const saveButton = document.querySelector(".inbox-queue__editor .btn--primary") as HTMLButtonElement;
93 saveButton.focus();
94 saveButton.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
95 await flush();
96 });
97 assert.equal(Boolean(document.querySelector(".inbox-queue__editor")), false, "Escape also exits when an editor action has keyboard focus");
98 assert.equal(document.querySelector(".composer-context__item"), attachmentCard, "cancelling restores the same attachment");
99 assert.equal((document.querySelector("textarea.composer__input") as HTMLTextAreaElement).value, "main draft survives editing");
100 failSave = true;
101 await act(async () => { (document.querySelector('.inbox-queue__row[data-item-id="queue-save"] button[aria-label="Edit"]') as HTMLButtonElement).click(); await flush(); });
102 await act(async () => {
103 document.querySelector(".inbox-queue__editor textarea")!.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", ctrlKey: true, bubbles: true }));
104 await flush();
105 });
106 assert.equal(document.querySelector('.inbox-queue__editor [role="alert"]')?.textContent, "This message was edited elsewhere. Your changes were kept.");
107 assert.equal(document.querySelector(".composer-wrap")?.getAttribute("data-queue-editing"), "true", "failed save keeps edit mode and original text");
108 assert.equal((document.querySelector(".inbox-queue__editor textarea") as HTMLTextAreaElement).value, textarea.value);
109 failSave = false;
110 await act(async () => {
111 const reload = [...document.querySelectorAll<HTMLButtonElement>(".inbox-queue__editor button")].find(button => button.textContent === "Load latest version")!;
112 reload.click(); await flush();
113 });
114 departOnSave = true;
115 await act(async () => {
116 document.querySelector(".inbox-queue__editor textarea")!.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", ctrlKey: true, bubbles: true }));
117 await flush();
118 });
119 assert.equal(document.querySelector('.inbox-queue__row[data-item-id="queue-save"]'), null);
120 assert.ok(document.querySelector('.inbox-queue__departed textarea'), "dispatch or deletion cannot unmount unsaved text");
121 assert.ok(document.querySelector('.inbox-queue__departed [role="alert"]'));
122 await act(async () => { (document.querySelector(".inbox-queue__running button") as HTMLButtonElement).click(); await flush(); });
123 assert.deepEqual(cancelled, [], "stop during editing leaves the queue intact");
124 assert.ok(document.querySelector('.inbox-queue__departed textarea'), "stop does not discard edit text");
125 await act(async () => {
126 document.querySelector(".inbox-queue__editor textarea")!.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
127 await flush();
128 });
129 await act(async () => { (document.querySelector(".composer__btn--stop") as HTMLButtonElement).click(); await flush(); });
130 assert.deepEqual(cancelled, [], "stop leaves the durable queue intact");
131 otherHost = true;
132 await view.rerender({ inboxHostId: "second-host", inboxWorkspace: "/repo" });
133 await act(async () => { await flush(); });
134 assert.ok(document.querySelector('.inbox-queue__row[data-item-id="other-host"]'), "a different host may start at a lower revision for the same session path");
135 assert.equal(document.querySelector('.inbox-queue__row[data-item-id="queue-save"]'), null, "the previous host's queue cannot leak into the new host");
136 await act(async () => { view.root.unmount(); });
137 dom.window.close();
138 console.log("PASS composer queue: default follow-up, explicit guide, in-place editing, draft preservation, stop");
139
139 lines Plain Text