返回 DeepSeek-Reasonix
topic-removal-controller.test.tsx
根目录 / desktop / frontend / src / __tests__ / topic-removal-controller.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 { useProjectTreeArchiveController } from "../lib/projectTreeArchive";
6 import { LocaleProvider } from "../lib/i18n";
7 import { installDesktopHostStub } from "./desktopHostStub";
8 import type { TopicRemovalRequest, TopicRemovalResult } from "../generated/desktopContract.generated";
9
10 const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost" });
11 Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, IS_REACT_ACT_ENVIRONMENT: true });
12 let disposition = "discard_placeholder";
13 let confirm = false;
14 let confirmations = 0;
15 let inspections = 0;
16 let removed = 0;
17 let refreshed = 0;
18 const requests: TopicRemovalRequest[] = [];
19 const errors: string[] = [];
20 const invalidated: string[] = [];
21 let response: TopicRemovalResult = { committed: true, disposition, retryable: false };
22 const stub = installDesktopHostStub({
23 InspectTopicRemoval: async (target: { topicId: string }) => { inspections++; return { target: { workspaceId: "global", topicId: target.topicId }, disposition, allowed: true, token: target.topicId }; },
24 ConfirmAction: async () => { confirmations++; return confirm; },
25 RemoveTopic: async (request: TopicRemovalRequest) => { requests.push(request); return response; },
26 TrashTopic: async () => { throw new Error("must use the typed removal receipt"); },
27 });
28 let owner!: ReturnType<typeof useProjectTreeArchiveController>;
29 const props: Parameters<typeof useProjectTreeArchiveController>[0] = {
30 treeRef: { current: [{ key: "fixture", kind: "project", label: "Fixture", root: "/fixture", children:
31 ["empty", "named"].map(topicId => ({ key: topicId, topicId, kind: "topic", label: topicId })) }] },
32 invalidateProjectTopicLists: key => { invalidated.push(key); },
33 refreshRef: { current: async () => { refreshed++; } },
34 optimisticallyRemoveTopic: () => { removed++; }, optimisticallyRemoveSession: () => {}, closeMenu: () => {},
35 showToast: (message: string) => { errors.push(message); },
36 };
37 function Probe() { owner = useProjectTreeArchiveController(props); return null; }
38 const root = createRoot(document.getElementById("root")!);
39 try {
40 await act(async () => root.render(<LocaleProvider><Probe /></LocaleProvider>));
41 await act(async () => { await owner.trashTopic("empty"); });
42 assert.equal(confirmations, 0, "default empty placeholders do not require confirmation");
43 assert.equal(removed, 1);
44 assert.deepEqual(invalidated, ["fixture"], "committed removal retires the owning list");
45 disposition = "archive_placeholder";
46 await act(async () => { await owner.trashTopic("named"); });
47 assert.equal(confirmations, 1);
48 assert.equal(requests.length, 1, "cancel does not mutate");
49 assert.equal(removed, 1, "cancel never installs a success tombstone");
50 assert.equal(invalidated.length, 1, "cancellation leaves list ownership intact");
51 confirm = true;
52 response = { committed: false, disposition, retryable: true, errorCode: "operation_failed" };
53 await act(async () => { await owner.trashTopic("named"); });
54 assert.equal(removed, 1, "partial/failed mutations stay authoritative until recovery reload");
55 assert.equal(errors.length, 1);
56 assert.equal(invalidated.length, 1, "failed removal does not commit list invalidation");
57 const inspectedBeforeRetry = inspections;
58 response = { committed: true, disposition, retryable: false, recoveryEntryId: "topic-removal:receipt" };
59 await act(async () => { await owner.trashTopic("named"); });
60 assert.equal(inspections, inspectedBeforeRetry, "retry works even after a partial removal hid the metadata");
61 assert.deepEqual(requests[1], requests[2], "retry retains operation identity and confirmation token");
62 assert.equal(removed, 2);
63 assert.deepEqual(invalidated, ["fixture", "fixture"], "successful retry invalidates through the shared list owner");
64 assert.ok(refreshed >= 3);
65 await act(async () => root.unmount());
66 console.log("PASS topic removal: confirmation, cancellation, typed commit and idempotent retry");
67 } finally { stub.uninstall(); dom.window.close(); }
68
68 lines Plain Text