返回 DeepSeek-Reasonix
inbox-queue-editor.test.tsx
根目录 / desktop / frontend / src / __tests__ / inbox-queue-editor.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { installDom, installBridgeApp } from "./composerInboxHarness";
5 import { useInboxQueueEditor } from "../lib/useInboxQueueEditor";
6 import { inboxQueueDrafts } from "../lib/inboxQueueDrafts";
7 import { queueActionAnchor, queueMoveAnchor, type InboxQueueRequest } from "../lib/inboxQueueCommands";
8 import { createInboxQueuePreview } from "../lib/inboxQueuePreview";
9 import type { InboxTarget } from "../lib/pendingFollowup";
10
11 const dom = installDom();
12 globalThis.sessionStorage = dom.window.sessionStorage;
13 const target: InboxTarget = { tabId: "tab-a", sessionPath: "session-a", generation: 7, selection: 3, remote: true };
14 const fixture = createInboxQueuePreview();
15 const requests: InboxQueueRequest[] = [];
16 let loseSaveReply = false;
17 installBridgeApp({
18 CaptureInboxTarget: async () => target,
19 InboxQueueForTarget: async (captured: InboxTarget, request: InboxQueueRequest) => {
20 assert.deepEqual(captured, target);
21 requests.push(request);
22 const result = await fixture.command(captured, request);
23 if (request.kind === "edit" && loseSaveReply) throw new Error("reply lost after commit");
24 return result;
25 },
26 });
27 let editor!: ReturnType<typeof useInboxQueueEditor>;
28 function Harness({ scope }: { scope: string }) {
29 editor = useInboxQueueEditor(scope, target.tabId, target.sessionPath, () => {}, () => {});
30 return <p>{editor.drafts.edit?.value}</p>;
31 }
32 const root = createRoot(document.getElementById("root")!);
33 const render = (scope: string) => act(async () => { root.render(<Harness key={scope} scope={scope} />); });
34 await render("queue-test-a");
35 await act(async () => { await editor.edit("queue-layout"); });
36 await act(async () => { editor.close(); });
37 assert.equal(editor.drafts.edit, undefined, "opening and cancelling without changes does not create a retained draft");
38 assert.equal(editor.open, false);
39 await act(async () => { await editor.edit("queue-save"); });
40 const original = editor.drafts.edit!;
41 assert.ok(original.value.includes("\n完成后验证"), "read includes the full multiline body");
42 assert.equal(original.references.length, 2);
43 const longText = " 保留空白\n" + "完整正文".repeat(200) + "\n结束 ";
44 await act(async () => { editor.update(longText); editor.close(); });
45 await render("queue-test-b");
46 assert.equal(editor.drafts.edit, undefined, "draft is scoped to its original session");
47 await render("queue-test-a");
48 assert.equal(editor.drafts.edit?.value, longText, "remount retains unsaved input");
49 await act(async () => { await editor.mutate({ kind: "pause", paused: true }); await editor.save(); });
50 assert.equal(editor.drafts.edit, undefined);
51 assert.equal(fixture.snapshot().paused, true, "saving does not resume the queue");
52 assert.deepEqual(fixture.snapshot().items.map(item => item.id), ["queue-layout", "queue-save", "queue-tests"]);
53 assert.equal(fixture.snapshot().items[1].preview, longText);
54
55 await act(async () => { await editor.edit("queue-save"); editor.update("my conflicting edits"); });
56 await fixture.command(target, { kind: "edit", itemId: "queue-save", text: "another window", contentVersion: editor.drafts.edit!.contentVersion });
57 await act(async () => { await editor.save(); });
58 assert.equal(editor.drafts.edit?.value, "my conflicting edits");
59 assert.equal(editor.drafts.edit?.reason, "content_changed");
60 await act(async () => { await editor.reload(); });
61 assert.equal(editor.drafts.edit?.value, "another window");
62 assert.equal(editor.drafts.recovered.at(-1)?.value, "my conflicting edits");
63
64 await act(async () => { editor.update("save committed but reply lost"); });
65 loseSaveReply = true;
66 const before = requests.length;
67 await act(async () => { await editor.save(); });
68 assert.deepEqual(requests.slice(before).map(r => r.kind), ["edit", "snapshot"], "ambiguous save reconciles once without replay");
69 assert.equal(editor.drafts.edit?.reason, "unconfirmed");
70 assert.equal(editor.drafts.edit?.value, "save committed but reply lost");
71 let restored: string | undefined;
72 await act(async () => { restored = editor.recover(); });
73 assert.equal(restored, "save committed but reply lost");
74 assert.equal(editor.drafts.edit, undefined);
75 assert.equal(editor.drafts.recovered.at(-1)?.value, restored);
76
77 // Pointer and keyboard actions share exactly the same stable anchor contract.
78 assert.equal(queueMoveAnchor(["a", "b", "c"], "a", "b"), "c");
79 assert.equal(queueActionAnchor(["a", "b", "c"], "a", "down"), "c");
80 assert.equal(queueMoveAnchor(["a", "b", "c"], "a", "c"), null);
81 assert.equal(queueActionAnchor(["a", "b", "c"], "c", "first"), "a");
82 assert.equal(queueMoveAnchor(["a", "b", "c"], "a", "missing"), undefined);
83 await act(async () => { inboxQueueDrafts.set("queue-test-a", { recovered: [] }); root.unmount(); });
84 dom.window.close();
85 console.log("PASS queue editor: full body, remount, pause, conflict, recovery and ambiguous save");
86
86 lines Plain Text