返回 DeepSeek-Reasonix
remote-file-navigation-races.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-file-navigation-races.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { renderFilesWorkspace, waitFor } from "./workspace-panel-test-harness";
4 import { RemotePanel } from "../components/RemotePanel";
5 import { LocaleProvider } from "../lib/i18n";
6 import { useRemoteStore } from "../store/remote";
7
8 function deferred<T>() {
9 let resolve!: (value: T) => void;
10 let reject!: (error: Error) => void;
11 const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
12 return { promise, resolve, reject };
13 }
14 type Preview = { path: string; body: string; size: number; mtimeUnix: number; binary: boolean; truncated: boolean };
15 const pending = new Map<string, ReturnType<typeof deferred<Preview>>>();
16 const writes: Array<{ path: string; body: string }> = [];
17 const pendingWrites: Array<ReturnType<typeof deferred<{ conflict: boolean; newMtimeUnix: number }>>> = [];
18 const { dom, root } = await renderFilesWorkspace({
19 ListRemoteDir: async () => ["a.txt", "b.txt"].map(name => ({ name, path: name, isDir: false, size: 5 })),
20 ReadRemoteFile: (_host, path) => {
21 const read = deferred<Preview>(); pending.set(path, read); return read.promise;
22 },
23 WriteRemoteFile: (_host, path, body) => {
24 writes.push({ path, body });
25 const write = deferred<{ conflict: boolean; newMtimeUnix: number }>();
26 pendingWrites.push(write);
27 return write.promise;
28 },
29 });
30 await act(async () => {
31 useRemoteStore.setState({ explorerHostId: "remote", explorerTab: "files", statuses: { remote: { state: "connected" } } });
32 root.render(<LocaleProvider><RemotePanel onClose={() => {}} tabId="one" dockTabId="dock" /></LocaleProvider>);
33 });
34 await waitFor("remote tree", () => document.querySelectorAll(".remote-tree__row").length === 2);
35 const click = async (name: string) => {
36 const button = [...document.querySelectorAll<HTMLButtonElement>(".remote-tree__row")].find(node => node.textContent === name)!;
37 await act(async () => button.click());
38 await waitFor(`read ${name}`, () => pending.has(name));
39 };
40 await click("a.txt");
41 await click("b.txt");
42 const result = (path: string): Preview => ({ path, body: `CONTENT ${path}`, size: 10, mtimeUnix: 1, binary: false, truncated: false });
43 await act(async () => pending.get("b.txt")!.resolve(result("b.txt")));
44 await waitFor("new body", () => document.body.textContent?.includes("CONTENT b.txt") === true);
45 await act(async () => pending.get("a.txt")!.resolve(result("a.txt")));
46 assert(!document.body.textContent?.includes("CONTENT a.txt"));
47 assert(document.body.textContent?.includes("CONTENT b.txt"));
48
49 // A write issued for one file must not touch the file the panel moved on to.
50 await click("a.txt");
51 await act(async () => pending.get("a.txt")!.resolve(result("a.txt")));
52 await waitFor("a reloaded", () => document.body.textContent?.includes("CONTENT a.txt") === true);
53 const edit = [...document.querySelectorAll<HTMLButtonElement>(".remote-file-view__toolbar button")]
54 .find(button => /edit/i.test(button.textContent ?? ""))!;
55 await act(async () => { edit.click(); });
56 // jsdom does not deliver a text input event to React's handler in this harness;
57 // the mounted element's own props are the same contract the browser drives.
58 const textarea = document.querySelector<HTMLTextAreaElement>(".remote-file-view__editor")!;
59 const propsKey = Object.keys(textarea).find((key) => key.startsWith("__reactProps"));
60 const onChange = propsKey
61 ? (textarea as unknown as Record<string, { onChange?: (event: { target: { value: string } }) => void }>)[propsKey]?.onChange
62 : undefined;
63 assert(onChange, "the editor exposes its change handler");
64 await act(async () => { onChange!({ target: { value: "DRAFT A" } }); });
65 const save = [...document.querySelectorAll<HTMLButtonElement>(".remote-file-view__toolbar button")]
66 .find(button => /save/i.test(button.textContent ?? ""))!;
67 assert.equal(save.disabled, false, "an edited file can be saved");
68 await act(async () => save.click());
69 await waitFor("write issued", () => writes.length === 1);
70 assert.deepEqual(writes[0], { path: "a.txt", body: "DRAFT A" }, "the write targets the file it was issued for");
71 await click("b.txt");
72 await act(async () => pending.get("b.txt")!.resolve(result("b.txt")));
73 await waitFor("replaced file", () => document.body.textContent?.includes("CONTENT b.txt") === true);
74 await act(async () => pendingWrites[0]!.resolve({ conflict: false, newMtimeUnix: 2 }));
75 assert(document.body.textContent?.includes("CONTENT b.txt"), "a receipt for another file cannot overwrite the one on screen");
76 assert(!document.body.textContent?.includes("DRAFT A"), "a receipt for another file cannot leak its draft");
77 assert.equal(document.querySelector(".remote-file-view__conflict"), null, "a receipt for another file cannot raise its conflict");
78
79 // A disconnected host reports the failure in the panel rather than a stale view.
80 await act(async () => useRemoteStore.setState({ statuses: { remote: { state: "stopped" } } }));
81 await waitFor("disconnected hint", () => document.querySelector(".remote-panel__hint") !== null);
82 assert.equal(document.querySelector(".remote-file-view"), null, "a disconnected host shows no file view");
83 assert(!document.body.textContent?.includes("CONTENT b.txt"), "a disconnected host shows no stale preview");
84
85 await act(async () => root.unmount());
86 await act(async () => pending.get("a.txt")!.resolve(result("a.txt")));
87 assert.equal(document.querySelector(".remote-file-view"), null);
88 dom.window.close();
89 console.log("PASS actual remote panel: out-of-order reads, save isolation, late completion after disposal");
90
90 lines Plain Text