返回 DeepSeek-Reasonix
trash-management.test.tsx
根目录 / desktop / frontend / src / __tests__ / trash-management.test.tsx
1 import { registerHooks } from "node:module";
2 registerHooks({ resolve(specifier, context, nextResolve) { return specifier.endsWith(".svg") ? nextResolve("./asset-stub-for-tests.ts", { ...context, parentURL: import.meta.url }) : nextResolve(specifier, context); } });
3 import assert from "node:assert/strict";
4 import { managementDom } from "../test-support/managementDom";
5 import type { SessionMeta } from "../lib/types";
6 import { installDesktopHostStub } from "./desktopHostStub";
7 const dom = managementDom();
8 const { default: React, act } = await import("react");
9 const { createRoot } = await import("react-dom/client");
10 const { LocaleProvider } = await import("../lib/i18n");
11 const { TrashPage } = await import("../components/TrashPage");
12 type Row = { id: string; ref: { hostId: string; sessionId: string }; title: string; workspaceId: string; workspaceTitle: string; archivedAt: number; health: string; canPreview: boolean; canRestore: boolean; canPurge: boolean };
13 const row = (id: string): Row => ({ id, ref: { hostId: "local", sessionId: id }, title: id, workspaceId: "global", workspaceTitle: "Global", archivedAt: 0, health: "ready", canPreview: true, canRestore: true, canPurge: true });
14 let rows = [row("a"), row("b"), row("c")];
15 let latePreview!: () => void;
16 let listFails = false;
17 let throwConflict = false;
18 const calls: string[] = [], requests: string[] = [];
19 const fullRequests: string[] = [];
20 const terminal = new Set<string>();
21 installDesktopHostStub({
22 ListTrashEntries: async () => { if (listFails) throw new Error("read failure"); return { items: rows, generation: 7 }; },
23 ListRecoveryEntries: async () => ({ items: [{ id: "protected", title: "Protected history", canRestore: true, canPreview: true }], generation: 7 }),
24 ReadSessionHistory: (ref: {sessionId:string}) => ref.sessionId === "a" ? new Promise(resolve => { latePreview = () => resolve({ messages: [{ messageId: "old", role: "user", content: "stale preview" }] }); }) : Promise.resolve({ messages: [{ messageId: "b", role: "user", content: "B history" }] }),
25 ApplySessionLifecycle: async (request: { operationId:string; action:string; targets:{ref:{sessionId:string}}[] }) => {
26 requests.push(request.operationId);
27 fullRequests.push(JSON.stringify(request));
28 if (throwConflict) {
29 throwConflict = false;
30 throw new Error("workspace mutation conflicts with persisted state");
31 }
32 const items = request.targets.map(target => {
33 const id = target.ref.sessionId;
34 // Completed children are not executed again when the whole command retries.
35 if (!rows.some(row => row.id === id)) return { target, ref: target.ref, committed: true, workspaceId: "global" };
36 if (terminal.has(id)) return { target, ref: target.ref, committed: false, retryable: false, errorCode: "state_conflict", workspaceId: "global" };
37 calls.push(`${request.action}:${id}`);
38 const conflict = request.action === "purge" && id === "c";
39 const committed = request.action === "restore" || (id !== "b" && !conflict);
40 if (committed) rows = rows.filter(row => row.id !== id);
41 if (conflict) terminal.add(id);
42 if (request.action === "restore") listFails = true;
43 return { target, ref: target.ref, committed, retryable: !committed && !conflict,
44 errorCode: committed ? "" : conflict ? "state_conflict" : "operation_failed", workspaceId: "global" };
45 });
46 return { operationId: request.operationId, generation: 8, committed: items.every(item => item.committed), items };
47 },
48 });
49 const root = createRoot(document.getElementById("root")!);
50 const render = (active = true) => <LocaleProvider><TrashPage active={active} onBack={() => {}} onOpenSession={async () => {}} list={async ():Promise<SessionMeta[]> => { throw new Error("legacy list must not be queried"); }} purge={async () => { throw new Error("legacy purge must not be called"); }} restore={async () => {}} /></LocaleProvider>;
51 const button = (text: string) => Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find(node => node.textContent?.trim() === text)!;
52 await act(async () => root.render(render()));
53 assert.equal(document.querySelector('[aria-pressed="true"]'), null, "no archived/deleted sub-tabs");
54 assert.equal(document.querySelectorAll('.archived-sessions__row').length, 3);
55 await act(async () => (document.querySelectorAll('.archived-sessions__open')[0] as HTMLButtonElement).click());
56 await act(async () => (document.querySelectorAll('.archived-sessions__open')[1] as HTMLButtonElement).click());
57 await act(async () => latePreview());
58 assert.ok(document.querySelector('.archived-sessions__preview')?.textContent?.includes("B history"));
59 assert.ok(!document.body.textContent?.includes("stale preview"));
60 await act(async () => (document.querySelector('.history-clear') as HTMLButtonElement).click());
61 assert.ok(document.querySelector('[role="dialog"]')?.textContent?.includes("these 3 archived conversations"));
62 assert.equal(document.activeElement?.textContent, "Cancel");
63 await act(async () => button("Permanently delete").click());
64 assert.deepEqual(calls, ["purge:a", "purge:b", "purge:c"]);
65 assert.ok(document.body.textContent?.includes("changed state. Refresh and try again"));
66 assert.equal(document.querySelectorAll('.archived-sessions__row').length, 2);
67 await act(async () => button("Retry failed items").click());
68 assert.deepEqual(calls, ["purge:a", "purge:b", "purge:c", "purge:b"]);
69 assert.equal(requests[0], requests[1], "retry keeps the durable operation ID");
70 assert.equal(fullRequests[0], fullRequests[1], "mixed retry keeps every original target and the original version");
71 await act(async () => button("Restore").click());
72 assert.ok(document.body.textContent?.includes("Operation completed. Refresh failed"));
73 assert.equal(document.querySelectorAll('.archived-sessions__row').length, 1);
74 listFails = false;
75 throwConflict = true;
76 await act(async () => (document.querySelector('.archived-sessions__delete') as HTMLButtonElement).click());
77 await act(async () => button("Permanently delete").click());
78 assert.ok(document.body.textContent?.includes("changed state. Refresh and try again"), "top-level conflict uses the refresh guidance");
79 assert.equal(button("Retry").textContent?.trim(), "Retry", "terminal conflict is not eligible for request retry");
80 await act(async () => root.render(render(false)));
81 assert.equal(document.querySelector('[role="dialog"]'), null);
82 await act(async () => root.unmount());
83 dom.window.close();
84 console.log("PASS unified trash, stale preview, confirmation, conflict refresh, failed-only retry and committed refresh failure");
85
85 lines Plain Text