返回 DeepSeek-Reasonix
inbox-queue-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / inbox-queue-lifecycle.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 { createInboxQueuePreview } from "../lib/inboxQueuePreview";
7 import type { InboxQueueRequest } from "../lib/inboxQueueCommands";
8 import type { InboxTarget } from "../lib/pendingFollowup";
9
10 const dom = installDom();
11 globalThis.sessionStorage = dom.window.sessionStorage;
12 const root = createRoot(document.getElementById("root")!);
13 let editor!: ReturnType<typeof useInboxQueueEditor>;
14 let applied = 0;
15 function Harness({ scope }: { scope: string }) {
16 editor = useInboxQueueEditor(scope, "tab-a", "session-a", () => { applied++; }, () => {});
17 return <p>{editor.drafts.edit?.value}</p>;
18 }
19 const render = (scope: string) => act(async () => { root.render(<Harness key={scope} scope={scope} />); });
20 function deferred() {
21 let resolve!: () => void;
22 const promise = new Promise<void>(done => { resolve = done; });
23 return { promise, resolve };
24 }
25 const target: InboxTarget = { tabId: "tab-a", sessionPath: "session-a", generation: 1, selection: 1, remote: true };
26 const failures: string[] = [];
27 async function check(name: string, test: () => Promise<void>) {
28 try { await test(); console.log(`PASS ${name}`); }
29 catch (error) { failures.push(`${name}: ${String(error)}`); }
30 }
31
32 await check("late save must not clear a remounted editor's new draft", async () => {
33 const fixture = createInboxQueuePreview();
34 const committed = deferred(), reply = deferred();
35 installBridgeApp({
36 CaptureInboxTarget: async () => target,
37 InboxQueueForTarget: async (captured: InboxTarget, request: InboxQueueRequest) => {
38 const result = await fixture.command(captured, request);
39 if (request.kind === "edit") { committed.resolve(); await reply.promise; }
40 return result;
41 },
42 });
43 await render("save-a");
44 await act(async () => { await editor.edit("queue-save"); editor.update("submitted text"); });
45 let save!: Promise<void>;
46 await act(async () => { save = editor.save(); await committed.promise; });
47 await render("save-b");
48 await render("save-a");
49 await act(async () => { editor.update("newer text after returning"); });
50 const before = applied;
51 await act(async () => { reply.resolve(); await save; });
52 assert.equal(editor.drafts.edit?.value, "newer text after returning");
53 assert.equal(applied, before, "unmounted owner cannot publish snapshots");
54 });
55
56 await check("target capture resolving after unmount must not issue a command", async () => {
57 const fixture = createInboxQueuePreview();
58 const captured = deferred(), release = deferred();
59 const requests: InboxQueueRequest[] = [];
60 installBridgeApp({
61 CaptureInboxTarget: async () => { captured.resolve(); await release.promise; return target; },
62 InboxQueueForTarget: async (binding: InboxTarget, request: InboxQueueRequest) => { requests.push(request); return fixture.command(binding, request); },
63 });
64 await render("capture-a");
65 let pause!: Promise<void>;
66 await act(async () => { pause = editor.mutate({ kind: "pause", paused: true }); await captured.promise; });
67 await render("capture-b");
68 await act(async () => { release.resolve(); await pause; });
69 assert.equal(requests.length, 0);
70 assert.equal(fixture.snapshot().paused, false);
71 });
72
73 await check("late read cannot replace an edit opened by the new owner", async () => {
74 const fixture = createInboxQueuePreview();
75 const read = deferred(), reply = deferred();
76 installBridgeApp({
77 CaptureInboxTarget: async () => target,
78 InboxQueueForTarget: async (binding: InboxTarget, request: InboxQueueRequest) => {
79 const result = await fixture.command(binding, request);
80 if (request.kind === "read" && request.itemId === "queue-save") { read.resolve(); await reply.promise; }
81 return result;
82 },
83 });
84 await render("read-a");
85 let pending!: Promise<void>;
86 await act(async () => { pending = editor.edit("queue-save"); await read.promise; });
87 await render("read-b");
88 await render("read-a");
89 await act(async () => { await editor.edit("queue-tests"); editor.update("new owner's draft"); });
90 const before = applied;
91 await act(async () => { reply.resolve(); await pending; });
92 assert.equal(editor.drafts.edit?.id, "queue-tests");
93 assert.equal(editor.drafts.edit?.value, "new owner's draft");
94 assert.equal(applied, before);
95 });
96
97 await check("late failed save does not reconcile or overwrite a replacement draft", async () => {
98 const fixture = createInboxQueuePreview();
99 const started = deferred(), reply = deferred();
100 const requests: InboxQueueRequest[] = [];
101 installBridgeApp({
102 CaptureInboxTarget: async () => target,
103 InboxQueueForTarget: async (binding: InboxTarget, request: InboxQueueRequest) => {
104 requests.push(request);
105 if (request.kind === "edit") { started.resolve(); await reply.promise; throw new Error("reply lost"); }
106 return fixture.command(binding, request);
107 },
108 });
109 await render("failure-a");
110 await act(async () => { await editor.edit("queue-save"); editor.update("submitted"); });
111 let pending!: Promise<void>;
112 await act(async () => { pending = editor.save(); await started.promise; });
113 await render("failure-b");
114 await render("failure-a");
115 await act(async () => { editor.update("preserve newer input"); });
116 await act(async () => { reply.resolve(); await pending; });
117 assert.equal(editor.drafts.edit?.value, "preserve newer input");
118 assert.equal(editor.drafts.edit?.reason, undefined);
119 assert.equal(requests.filter(request => request.kind === "snapshot").length, 0);
120 });
121
122 await check("load latest reacquires current selection instead of reusing stale target", async () => {
123 const fixture = createInboxQueuePreview();
124 let selection = 1;
125 installBridgeApp({
126 CaptureInboxTarget: async () => ({ ...target, selection }),
127 InboxQueueForTarget: async (binding: InboxTarget, request: InboxQueueRequest) => binding.selection !== selection
128 ? { outcome: "unavailable", reason: "session_changed", snapshot: fixture.snapshot("session-a") }
129 : fixture.command(binding, request),
130 });
131 await render("reload-a");
132 await act(async () => { await editor.edit("queue-save"); editor.update("keep this local edit"); });
133 selection = 2;
134 await act(async () => { await editor.save(); });
135 assert.equal(editor.drafts.edit?.reason, "session_changed");
136 await act(async () => { await editor.reload(); });
137 assert.equal(editor.drafts.edit?.reason, undefined);
138 assert.equal(editor.drafts.edit?.target.selection, 2);
139 assert.equal(editor.drafts.recovered.at(-1)?.value, "keep this local edit");
140 });
141
142 await act(async () => { root.unmount(); });
143 dom.window.close();
144 assert.deepEqual(failures, []);
145
145 lines Plain Text