返回 DeepSeek-Reasonix
manual-creation-recovery.test.tsx
根目录 / desktop / frontend / src / __tests__ / manual-creation-recovery.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { ManualSessionRecovery } from "../components/ManualSessionRecovery";
6 import { LocaleProvider } from "../lib/i18n";
7 import { manualCreationPresentation } from "../lib/manualCreationPresentation";
8 import type { ManualSessionCreationView } from "../generated/desktopContract.generated";
9 import { installDesktopHostStub } from "./desktopHostStub";
10
11 const item = { operationId: "recovery-test", scope: "global", phase: "starting", ref: { hostId: "local", sessionId: "current" } } as ManualSessionCreationView;
12 const progress = (status: string, slow = false) => ({ status, stage: "building_runtime", stageStartedAt: 1, elapsedMs: 0, slow });
13 assert.equal(manualCreationPresentation(item).key, "creation.preparing");
14 assert.equal(manualCreationPresentation({ ...item, progress: progress("future-status") }).key, "creation.preparing");
15 assert.equal(manualCreationPresentation({ ...item, progress: progress("running", true) }).key, "creation.slow");
16 for (const status of ["queued", "running", "waiting_lock", "retrying_storage"]) {
17 assert.deepEqual(manualCreationPresentation({ ...item, phase: "failed", progress: progress(status) }), { key: "creation.preparing" }, "host-owned recovery exposes no user decision");
18 }
19 assert.deepEqual(manualCreationPresentation({ ...item, progress: progress("waiting_workspace") }), { key: "creation.workspaceUnavailable" });
20 for (const code of ["workspace_removed", "workspace_changed", "creation_cancelled", "creation_owner_conflict"]) {
21 assert.equal(manualCreationPresentation({ ...item, phase: "failed", error: `session_operation:${code}:private detail` }).action, code.startsWith("workspace_") ? "project" : "new");
22 assert.equal(manualCreationPresentation({ ...item, phase: "failed", error: `session_operation:${code}:private detail`, progress: progress("queued") }).key, "creation.preparing", "active recovery progress supersedes an earlier failure");
23 }
24
25 const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost" });
26 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
27 let retryCount = 0;
28 let exports = 0;
29 let finish!: (view: ManualSessionCreationView) => void;
30 const reply = new Promise<ManualSessionCreationView>(resolve => { finish = resolve; });
31 const stub = installDesktopHostStub({
32 ListManualSessionCreations: async () => [{ ...item, progress: progress("waiting_lock") },
33 { ...item, operationId: "old", ref: { hostId: "local", sessionId: "other" }, workspaceRoot: "/private/old-project", phase: "failed" }],
34 RetryManualSessionCreation: async () => { retryCount++; return reply; },
35 ExportManualCreationDiagnostics: async () => { exports++; return "report.json"; },
36 });
37 const root = createRoot(document.getElementById("root")!);
38 try {
39 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery sessionId="current" /></LocaleProvider>));
40 assert.doesNotMatch(document.body.textContent!, /\/private\/old-project|global/, "background creation failures and internal scope labels never appear in the current composer");
41 assert.match(document.body.textContent!, /You can type now/);
42 assert.equal(document.querySelectorAll("button").length, 0, "ordinary waiting has no decisions or diagnostics");
43 await act(async () => root.render(null));
44 stub.commands.ListManualSessionCreations = async () => [{ ...item, phase: "failed", error: "session_operation:target_changed: /private/writer", progress: progress("blocked") }];
45 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery sessionId="current" /></LocaleProvider>));
46 assert.doesNotMatch(document.body.textContent!, /session_operation|\/private\/writer/, "internal failures stay out of the normal creation UI");
47 const retry = document.querySelector("button")!;
48 assert.equal(retry.textContent, "Try again");
49 assert.equal(document.querySelector("details")?.open, false);
50 await act(async () => [...document.querySelectorAll("button")].find(button => button.textContent === "Export creation diagnostics")!.click());
51 assert.equal(exports, 1);
52 act(() => { retry.click(); retry.click(); });
53 assert.equal(retryCount, 1, "duplicate retry is suppressed while the RPC is pending");
54 assert.equal(retry.disabled, true);
55 await act(async () => finish({ ...item, progress: progress("running") }));
56 assert.match(document.body.textContent!, /You can type now/);
57 assert.equal(document.querySelectorAll("button").length, 0);
58
59 await act(async () => root.render(null));
60 let lists = 0;
61 stub.commands.ListManualSessionCreations = async () => { lists++; return []; };
62 const attempt = { request: { operationId: item.operationId, workspaceId: "", scope: "global", workspaceRoot: "" },
63 operation: { ...item, surfaceReady: true, progress: progress("running") }, pending: true, failed: false };
64 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery attempt={attempt} /></LocaleProvider>));
65 assert.equal(lists, 0, "an explicit creation reuses its existing observer, without a catalog poll");
66 assert.match(document.body.textContent!, /You can type now/);
67 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery attempt={{ ...attempt, operation: { ...item, phase: "ready" } }} /></LocaleProvider>));
68 assert.equal(document.body.textContent, "", "readiness removes the notice");
69
70 let newCount = 0;
71 const terminal = { ...attempt, pending: false, operation: { ...item, phase: "failed", error: "session_operation:creation_cancelled:private" } };
72 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery attempt={terminal} onNew={async () => { newCount++; }} /></LocaleProvider>));
73 assert.equal(document.querySelector("button")?.textContent, "New session");
74 await act(async () => document.querySelector("button")!.click());
75 assert.equal(newCount, 1);
76 await act(async () => [...document.querySelectorAll("button")].find(button => button.textContent === "Close")!.click());
77 assert.equal(document.body.textContent, "", "dismissing the notice does not mutate or cancel its operation");
78 assert.equal(retryCount, 1);
79
80 await act(async () => root.render(null));
81 let projects = 0;
82 const moved = { ...terminal, operation: { ...item, phase: "failed", error: "session_operation:workspace_changed:private" } };
83 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery attempt={moved} onChooseProject={() => { projects++; }} /></LocaleProvider>));
84 assert.equal(document.querySelector("button")?.textContent, "Choose project");
85 await act(async () => document.querySelector("button")!.click());
86 assert.equal(projects, 1);
87
88 await act(async () => root.render(null));
89 const blocked = { ...item, phase: "failed", progress: progress("blocked") };
90 stub.commands.ListManualSessionCreations = async () => [blocked];
91 let pollStarted!: () => void;
92 const polling = new Promise<void>(resolve => { pollStarted = resolve; });
93 let releasePoll!: (view: ManualSessionCreationView) => void;
94 stub.commands.GetManualSessionCreation = () => { pollStarted(); return new Promise(resolve => { releasePoll = resolve; }); };
95 stub.commands.RetryManualSessionCreation = async () => ({ ...item, progress: progress("running") });
96 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery sessionId="current" /></LocaleProvider>));
97 await act(async () => { await polling; });
98 await act(async () => document.querySelector("button")!.click());
99 assert.match(document.body.textContent!, /You can type now/);
100 await act(async () => releasePoll(blocked));
101 assert.match(document.body.textContent!, /You can type now/, "an earlier blocked read cannot overwrite an accepted retry");
102
103 await act(async () => root.render(null));
104 let releaseOld!: (items: ManualSessionCreationView[]) => void;
105 stub.commands.ListManualSessionCreations = () => new Promise(resolve => { releaseOld = resolve; });
106 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery key="old-navigation" sessionId="current" /></LocaleProvider>));
107 await act(async () => root.render(<LocaleProvider><ManualSessionRecovery key="new-navigation" attempt={{ ...attempt, operation: { ...item, phase: "ready" } }} /></LocaleProvider>));
108 await act(async () => releaseOld([{ ...item, phase: "failed" }]));
109 assert.equal(document.body.textContent, "", "a late failure from the previous selection cannot reappear");
110 console.log("PASS selected-session notices, one observer, coalesced retry, terminal action, dismissal and optional diagnostics");
111 } finally {
112 await act(async () => root.unmount());
113 stub.uninstall();
114 dom.window.close();
115 }
116
116 lines Plain Text