返回 DeepSeek-Reasonix
approval-modal-file-reference.test.tsx
根目录 / desktop / frontend / src / __tests__ / approval-modal-file-reference.test.tsx
1 // Run: tsx src/__tests__/approval-modal-file-reference.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React from "react";
5 import { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import { ApprovalModal } from "../components/ApprovalModal";
8 import { activeFileReferenceToken, pickInlineFileReference } from "../components/FileReferenceMenu";
9 import { LocaleProvider, preloadDetectedLocale } from "../lib/i18n";
10 import type { AppBindings } from "../lib/bridge";
11 import type { WireApproval } from "../lib/types";
12 import { installDesktopHostStub } from "./desktopHostStub";
13
14 let passed = 0;
15 let failed = 0;
16
17 function ok(value: boolean, label: string) {
18 if (value) {
19 process.stdout.write(` PASS ${label}\n`);
20 passed += 1;
21 } else {
22 process.stdout.write(` FAIL ${label}\n`);
23 failed += 1;
24 }
25 }
26
27 function eq(actual: unknown, expected: unknown, label: string) {
28 if (actual === expected) ok(true, label);
29 else ok(false, `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
30 }
31
32 function flushTimers(ms = 0): Promise<void> {
33 return new Promise((resolve) => setTimeout(resolve, ms));
34 }
35
36 async function waitFor(label: string, predicate: () => boolean, timeoutMs = 1000) {
37 const start = Date.now();
38 while (Date.now() - start < timeoutMs) {
39 if (predicate()) return;
40 await act(async () => {
41 await flushTimers(20);
42 });
43 }
44 ok(false, label);
45 }
46
47 function installDom(language = "en-US") {
48 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
49 pretendToBeVisual: true,
50 url: "http://localhost/",
51 });
52 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
53 globalThis.window = dom.window as unknown as Window & typeof globalThis;
54 globalThis.document = dom.window.document;
55 Object.defineProperty(dom.window.navigator, "language", { configurable: true, value: language });
56 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
57 globalThis.Node = dom.window.Node;
58 globalThis.Element = dom.window.Element;
59 globalThis.HTMLElement = dom.window.HTMLElement;
60 globalThis.HTMLTextAreaElement = dom.window.HTMLTextAreaElement;
61 globalThis.Event = dom.window.Event;
62 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
63 globalThis.InputEvent = dom.window.InputEvent;
64 globalThis.MouseEvent = dom.window.MouseEvent;
65 globalThis.localStorage = dom.window.localStorage;
66 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
67 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
68 globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
69 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value: () => {} });
70 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value: () => {} });
71 return dom;
72 }
73
74 function mockApp(methods: Partial<AppBindings>) {
75 installDesktopHostStub(({
76 main: {
77 App: {
78 ...methods,
79 ListDirForTab: methods.ListDirForTab ?? (async (_tabId: string, rel: string) => methods.ListDir?.(rel) ?? []),
80 SearchFileRefsForTab: methods.SearchFileRefsForTab ?? (async (_tabId: string, query: string) => methods.SearchFileRefs?.(query) ?? []),
81 } as Partial<AppBindings> as AppBindings,
82 },
83 }).main.App);
84 }
85
86 async function renderApproval(props: Partial<Parameters<typeof ApprovalModal>[0]> = {}) {
87 await preloadDetectedLocale();
88 const rootEl = document.getElementById("root");
89 if (!rootEl) throw new Error("missing root");
90 const root = createRoot(rootEl);
91 const revisions: string[] = [];
92 const activeStates: boolean[] = [];
93 const approval: WireApproval = {
94 id: "plan-approval",
95 tool: "exit_plan_mode",
96 subject: "Plan ready",
97 };
98 let currentProps: Parameters<typeof ApprovalModal>[0] = {
99 approval,
100 cwd: "/repo",
101 tabId: "tab-a",
102 onAnswer: () => undefined,
103 onRevisePlan: (text) => revisions.push(text),
104 onExitPlan: () => undefined,
105 onStop: () => undefined,
106 onRevisionActiveChange: (active) => activeStates.push(active),
107 ...props,
108 };
109 const paint = async (nextProps: Partial<Parameters<typeof ApprovalModal>[0]> = {}) => {
110 currentProps = { ...currentProps, ...nextProps };
111 await act(async () => {
112 root.render(
113 <LocaleProvider>
114 <ApprovalModal {...currentProps} />
115 </LocaleProvider>,
116 );
117 await flushTimers();
118 });
119 };
120 await paint();
121 return { root, revisions, activeStates, rerender: paint };
122 }
123
124 function actionButton(label: string): HTMLButtonElement {
125 const button = Array.from(document.querySelectorAll(".prompt-shelf__actions .prompt-action")).find((el) =>
126 el.textContent?.includes(label),
127 ) as HTMLButtonElement | undefined;
128 if (!button) throw new Error(`action button not found: ${label}`);
129 return button;
130 }
131
132 function confirmButton(): HTMLButtonElement {
133 const button = document.querySelector(".decision-confirm-bar__confirm") as HTMLButtonElement | null;
134 if (!button) throw new Error("confirm button did not render");
135 return button;
136 }
137
138 async function selectAndConfirm(label: string) {
139 await act(async () => {
140 actionButton(label).click();
141 await flushTimers();
142 });
143 await act(async () => {
144 confirmButton().click();
145 await flushTimers(220);
146 });
147 }
148
149 async function clickImmediateAction(label: string) {
150 await act(async () => {
151 actionButton(label).click();
152 await flushTimers();
153 });
154 }
155
156 console.log("\napproval modal file references");
157
158 {
159 const token = activeFileReferenceToken("please inspect @README\n");
160 eq(token?.raw, "README", "plan revision file trigger ignores an invisible trailing newline");
161 eq(
162 pickInlineFileReference("please inspect @README\n", token?.raw ?? null, token?.dir ?? "", { name: "README.md", isDir: false }),
163 "please inspect @README.md ",
164 "plan revision file selection removes an invisible trailing newline",
165 );
166 }
167
168 {
169 const dom = installDom("en-US");
170 const fileScopeCalls: string[] = [];
171 mockApp({
172 ListDirForTab: async (tabId) => {
173 fileScopeCalls.push(tabId);
174 return [{ name: "src", isDir: true }, { name: "README.md", isDir: false }];
175 },
176 SearchFileRefsForTab: async () => [],
177 });
178 const { root, revisions, rerender } = await renderApproval();
179
180 await clickImmediateAction("Revise plan");
181
182 const textarea = document.querySelector(".plan-revision__input") as HTMLTextAreaElement | null;
183 if (!textarea) throw new Error("plan revision textarea did not render");
184
185 await rerender({ insertRequest: { id: 1, text: "please inspect @" } });
186 await waitFor("plan revision @ text opens file suggestions", () => document.body.textContent?.includes("README.md") === true);
187
188 ok(document.body.textContent?.includes("README.md") === true, "plan revision @ text opens file suggestions");
189 ok(fileScopeCalls.every((tabId) => tabId === "tab-a"), "plan revision file suggestions stay scoped to the active tab");
190
191 const readmeButton = Array.from(document.querySelectorAll(".slashmenu__item")).find((button) => button.textContent?.includes("README.md")) as HTMLButtonElement | undefined;
192 if (!readmeButton) throw new Error("README file suggestion did not render");
193
194 await act(async () => {
195 readmeButton.dispatchEvent(new window.MouseEvent("mousedown", { bubbles: true, cancelable: true }));
196 await flushTimers();
197 });
198
199 eq(textarea.value, "please inspect @README.md ", "file suggestion completes inline in the plan revision");
200
201 const sendButton = Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.includes("Send update")) as HTMLButtonElement | undefined;
202 if (!sendButton) throw new Error("send revision button did not render");
203
204 await act(async () => {
205 sendButton.click();
206 await flushTimers(220);
207 });
208
209 eq(revisions.join(","), "please inspect @README.md", "submitted plan revision keeps the selected file reference");
210
211 await act(async () => {
212 root.unmount();
213 });
214 dom.window.close();
215 }
216
217 {
218 const dom = installDom("zh-CN");
219 mockApp({
220 ListDir: async () => [],
221 SearchFileRefs: async () => [],
222 });
223 const { root } = await renderApproval({
224 approval: {
225 id: "sandbox-escape-approval-zh",
226 tool: "sandbox_escape",
227 subject: "run unconfined once: go test ./...",
228 reason: "Windows does not provide an OS-level Bash sandbox for this command. Run it unconfined one time? This bypasses OS isolation for this command only.",
229 },
230 });
231
232 const text = document.body.textContent ?? "";
233 ok(text.includes("go test ./..."), "sandbox escape approval keeps the command visible in Chinese UI");
234 ok(!text.includes("仅本次不进沙箱运行:"), "sandbox escape approval removes the redundant scope prefix from the command block");
235 eq((text.match(/go test \.\/\.\.\./g) ?? []).length, 1, "sandbox escape approval renders the command once");
236 ok(text.includes("Windows 不提供这条命令所需的 OS 级 Bash 沙箱"), "sandbox escape approval localizes the retired Windows backend reason in Chinese UI");
237 ok(text.includes("允许一次"), "sandbox escape Chinese approval shows allow once");
238 ok(text.includes("本会话使用真实环境"), "sandbox escape Chinese approval shows session grant");
239 ok(text.includes("拒绝"), "sandbox escape Chinese approval shows deny");
240 ok(!text.includes("总是允许"), "sandbox escape Chinese approval hides persistent grant");
241
242 await act(async () => {
243 root.unmount();
244 });
245 dom.window.close();
246 }
247
248 {
249 const dom = installDom("zh-CN");
250 mockApp({
251 ListDir: async () => [],
252 SearchFileRefs: async () => [],
253 });
254 const { root } = await renderApproval({
255 approval: {
256 id: "sandbox-escape-runtime-approval-zh",
257 tool: "sandbox_escape",
258 subject: "run unconfined once: go test ./...",
259 reason: "The OS sandbox could not start this command. Run it unconfined one time? This bypasses OS isolation for this command only.",
260 },
261 });
262
263 const text = document.body.textContent ?? "";
264 ok(text.includes("OS 沙箱无法启动这条命令"), "sandbox escape approval localizes the runtime failure reason in Chinese UI");
265
266 await act(async () => {
267 root.unmount();
268 });
269 dom.window.close();
270 }
271
272 {
273 const dom = installDom("zh-CN");
274 mockApp({
275 ListDir: async () => [],
276 SearchFileRefs: async () => [],
277 });
278 const { root } = await renderApproval({
279 approval: {
280 id: "memory-approval-zh",
281 tool: "remember",
282 subject: "Save/update memory \"prefers-vitest\" [user]: Preferred test framework | body: Use Vitest for frontend tests.",
283 },
284 });
285
286 const text = document.body.textContent ?? "";
287 ok(text.includes("保存记忆"), "remember approval localizes tool label in Chinese UI");
288 ok(text.includes("保存/更新记忆 \"prefers-vitest\" [user]"), "remember approval localizes subject prefix in Chinese UI");
289 ok(text.includes("正文: Use Vitest for frontend tests."), "remember approval localizes body label in Chinese UI");
290
291 await act(async () => {
292 root.unmount();
293 });
294 dom.window.close();
295 }
296
297 {
298 const dom = installDom("zh-CN");
299 mockApp({
300 ListDir: async () => [],
301 SearchFileRefs: async () => [],
302 });
303 const { root } = await renderApproval({
304 approval: {
305 id: "plan-mode-read-only-command-zh",
306 tool: "plan_mode_read_only_command",
307 subject: "Trust \"gh issue view\" as a read-only command prefix while planning\nCommand: gh issue view 5867 --json title",
308 reason: "This bash command is not in Reasonix's built-in read-only set. Confirm only if this exact prefix is read-only for planning and research. Permission presets cannot answer this trust prompt.",
309 },
310 });
311
312 const text = document.body.textContent ?? "";
313 ok(text.includes("计划模式只读命令"), "plan-mode read-only command approval localizes tool label in Chinese UI");
314 ok(text.includes("在计划模式中信任 \"gh issue view\" 为只读命令前缀"), "plan-mode read-only command approval localizes subject in Chinese UI");
315 ok(text.includes("当前权限模式之外"), "plan-mode read-only command approval localizes reason in Chinese UI");
316
317 await act(async () => {
318 root.unmount();
319 });
320 dom.window.close();
321 }
322
323 {
324 const dom = installDom("zh-CN");
325 mockApp({
326 ListDir: async () => [],
327 SearchFileRefs: async () => [],
328 });
329 const { root } = await renderApproval({
330 approval: {
331 id: "dynamic-bash-zh",
332 tool: "bash",
333 subject: "python3 -c \"print('hello')\"",
334 reason: "Matched permission rule: ask Bash(python3:*)\nThis command requests access outside the active permission preset. Review the target and approve only the precise scope required.",
335 },
336 });
337
338 const text = document.body.textContent ?? "";
339 ok(text.includes("命中权限规则:ask Bash(python3:*)"), "approval identifies the exact matched permission rule");
340 ok(text.includes("当前权限模式之外"), "Bash approval explains the active permission boundary in Chinese");
341 ok(text.includes("精确范围"), "Bash approval tells the user to grant only the required scope");
342
343 await act(async () => {
344 root.unmount();
345 });
346 dom.window.close();
347 }
348
349 {
350 const dom = installDom();
351 mockApp({
352 ListDir: async () => [],
353 SearchFileRefs: async () => [],
354 });
355 const { root, rerender } = await renderApproval();
356
357 await clickImmediateAction("Revise plan");
358
359 const textarea = document.querySelector(".plan-revision__input") as HTMLTextAreaElement | null;
360 if (!textarea) throw new Error("plan revision textarea did not render");
361 ok(textarea === document.activeElement, "opening plan revision focuses its textarea once");
362
363 const transcriptText = document.createElement("p");
364 transcriptText.tabIndex = -1;
365 transcriptText.textContent = "copy this plan text";
366 document.body.appendChild(transcriptText);
367 transcriptText.focus();
368 const range = document.createRange();
369 range.selectNodeContents(transcriptText);
370 const selection = document.getSelection();
371 selection?.removeAllRanges();
372 selection?.addRange(range);
373
374 // App refreshes tab metadata periodically; emulate callback churn from a parent rerender.
375 await rerender({ onRevisionActiveChange: () => undefined });
376
377 ok(document.activeElement === transcriptText, "parent rerender does not return focus to plan revision");
378 eq(document.getSelection()?.toString(), "copy this plan text", "parent rerender preserves transcript text selection");
379
380 transcriptText.remove();
381 await act(async () => {
382 root.unmount();
383 });
384 dom.window.close();
385 }
386
387 {
388 const dom = installDom();
389 mockApp({
390 ListDir: async () => [],
391 SearchFileRefs: async () => [],
392 });
393 const { root, activeStates, rerender } = await renderApproval();
394
395 await clickImmediateAction("Revise plan");
396
397 const textarea = document.querySelector(".plan-revision__input") as HTMLTextAreaElement | null;
398 if (!textarea) throw new Error("plan revision textarea did not render");
399
400 await rerender({ insertRequest: { id: 2, text: "@src/main.go" } });
401
402 eq(textarea.value, "@src/main.go", "workspace add-reference insert request targets the plan revision input");
403 ok(activeStates.includes(true), "plan revision reports itself as the active workspace insertion target");
404
405 await act(async () => {
406 root.unmount();
407 });
408 dom.window.close();
409 }
410
411 {
412 const dom = installDom();
413 mockApp({
414 ListDir: async () => [],
415 SearchFileRefs: async () => [],
416 });
417 const { root } = await renderApproval({
418 approval: {
419 id: "tool-approval",
420 tool: "bash",
421 subject: "npm run build\n\nRun the build command to verify frontend artifacts.",
422 },
423 });
424
425 const subject = document.querySelector(".approval-subject");
426 ok(subject != null, "tool approval shows its full subject by default");
427 eq(
428 subject?.textContent,
429 "npm run build\n\nRun the build command to verify frontend artifacts.",
430 "default-open tool approval keeps the complete subject visible",
431 );
432 eq(
433 (document.body.textContent?.match(/npm run build/g) ?? []).length,
434 1,
435 "tool approval renders the command once instead of repeating it in header metadata",
436 );
437 ok(document.querySelector(".prompt-shelf__meta") == null, "tool approval omits duplicate subject metadata");
438 // Subject is always visible; reason expands when short enough / via Details.
439 const actions = [...document.querySelectorAll(".prompt-shelf__actions .prompt-action")] as HTMLElement[];
440 eq(actions.length, 3, "ordinary tool approval exposes once, session, and deny options");
441 ok(actions[0]?.classList.contains("prompt-action--selected"), "default selection is allow once");
442 eq(
443 actions[1]?.getAttribute("title"),
444 "Allow matching calls until this session ends; resets on restart.",
445 "session option carries a native title fallback",
446 );
447 ok(document.querySelector(".decision-confirm-bar__confirm") != null, "decision surface shows an explicit confirm button");
448
449 await act(async () => {
450 actions[1].click();
451 await flushTimers();
452 });
453 ok(actions[1]?.classList.contains("prompt-action--selected"), "clicking an option only changes selection");
454 eq(
455 document.querySelectorAll(".prompt-action--selected").length >= 1,
456 true,
457 "selection state updates without submitting",
458 );
459
460 await act(async () => {
461 root.unmount();
462 });
463 dom.window.close();
464 }
465
466 {
467 const dom = installDom();
468 mockApp({
469 ListDir: async () => [],
470 SearchFileRefs: async () => [],
471 });
472 const answers: Array<[boolean, boolean, boolean]> = [];
473 const { root } = await renderApproval({
474 approval: {
475 id: "memory-approval",
476 tool: "remember",
477 subject: "Save/update memory \"prefers-vitest\": Preferred test framework",
478 },
479 onAnswer: (allow, session, persist) => answers.push([allow, session, persist]),
480 });
481
482 const text = document.body.textContent ?? "";
483 ok(text.includes("Allow once"), "fresh-human approval shows allow once");
484 ok(text.includes("Deny"), "fresh-human approval shows deny");
485 ok(!text.includes("Allow matching for this session"), "fresh-human approval hides session grant");
486 ok(!text.includes("Always allow matching"), "fresh-human approval hides persistent grant");
487 eq(
488 Array.from(document.querySelectorAll(".prompt-shelf__actions button")).map((button) => button.textContent).join("|"),
489 "1Allow onceAllow this call only; the next one asks again.|2DenyReject this call; the model sees the refusal and continues.",
490 "fresh-human approval keeps conventional allow/deny shortcut keys with inline consequences",
491 );
492
493 await act(async () => {
494 actionButton("Allow once").click();
495 await flushTimers();
496 });
497 eq(JSON.stringify(answers), "[]", "clicking allow once only selects; does not approve yet");
498
499 await act(async () => {
500 confirmButton().click();
501 await flushTimers(220);
502 });
503
504 eq(JSON.stringify(answers), JSON.stringify([[true, false, false]]), "fresh-human approval allows only once after confirm");
505
506 await act(async () => {
507 root.unmount();
508 });
509 dom.window.close();
510 }
511
512 {
513 const dom = installDom();
514 mockApp({
515 ListDir: async () => [],
516 SearchFileRefs: async () => [],
517 });
518 const answers: Array<[boolean, boolean, boolean]> = [];
519 const { root } = await renderApproval({
520 approval: {
521 id: "memory-approval-deny",
522 tool: "remember",
523 subject: "Save/update memory \"prefers-vitest\": Preferred test framework",
524 },
525 onAnswer: (allow, session, persist) => answers.push([allow, session, persist]),
526 });
527
528 await act(async () => {
529 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "2", bubbles: true, cancelable: true }));
530 await flushTimers();
531 });
532 eq(JSON.stringify(answers), "[]", "fresh-human numeric 2 only selects deny");
533
534 await act(async () => {
535 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
536 await flushTimers(220);
537 });
538
539 eq(JSON.stringify(answers), JSON.stringify([[false, false, false]]), "fresh-human Enter after digit 2 denies");
540
541 await act(async () => {
542 root.unmount();
543 });
544 dom.window.close();
545 }
546
547 {
548 const dom = installDom();
549 mockApp({
550 ListDir: async () => [],
551 SearchFileRefs: async () => [],
552 });
553 const answers: Array<{ allow: boolean; session: boolean; persist: boolean }> = [];
554 const { root } = await renderApproval({
555 approval: {
556 id: "sandbox-escape-approval",
557 tool: "sandbox_escape",
558 subject: "run unconfined once: go test ./...",
559 reason: "Windows sandbox failed while starting this command. Run it unconfined one time? This bypasses the OS sandbox for this command only.",
560 },
561 onAnswer: (allow, session, persist) => answers.push({ allow, session, persist }),
562 });
563
564 const text = document.body.textContent ?? "";
565 ok(text.includes("bash sandbox escape"), "sandbox escape approval uses a clear tool label");
566 ok(text.includes("Allow once"), "sandbox escape approval shows allow once");
567 ok(text.includes("Use real environment for this session"), "sandbox escape approval shows session grant");
568 ok(text.includes("Deny"), "sandbox escape approval shows deny");
569 ok(!text.includes("Always allow matching"), "sandbox escape approval hides persistent grant");
570 eq(document.querySelectorAll(".prompt-shelf__actions .prompt-action").length, 3, "sandbox escape keeps three options");
571
572 await act(async () => {
573 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
574 await flushTimers();
575 });
576 await act(async () => {
577 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
578 await flushTimers(220);
579 });
580 eq(JSON.stringify(answers), JSON.stringify([{ allow: true, session: true, persist: false }]), "sandbox escape Enter on selected session action grants session");
581
582 await act(async () => {
583 root.unmount();
584 });
585 dom.window.close();
586 }
587
588 {
589 const dom = installDom();
590 mockApp({
591 ListDir: async () => [],
592 SearchFileRefs: async () => [],
593 });
594 const answers: Array<{ allow: boolean; session: boolean; persist: boolean }> = [];
595 const { root } = await renderApproval({
596 approval: {
597 id: "sandbox-escape-deny-approval",
598 tool: "sandbox_escape",
599 subject: "run unconfined once: go test ./...",
600 reason: "Windows sandbox failed while starting this command. Run it unconfined one time? This bypasses the OS sandbox for this command only.",
601 },
602 onAnswer: (allow, session, persist) => answers.push({ allow, session, persist }),
603 });
604
605 await act(async () => {
606 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "3", bubbles: true, cancelable: true }));
607 await flushTimers();
608 });
609 eq(JSON.stringify(answers), "[]", "sandbox escape numeric 3 only selects deny");
610 await act(async () => {
611 document.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
612 await flushTimers(220);
613 });
614 eq(JSON.stringify(answers), JSON.stringify([{ allow: false, session: false, persist: false }]), "sandbox escape Enter after digit 3 denies");
615
616 await act(async () => {
617 root.unmount();
618 });
619 dom.window.close();
620 }
621
622 {
623 const dom = installDom("en-US");
624 const pending: Array<(entries: Array<{ name: string; isDir: boolean }>) => void> = [];
625 mockApp({
626 ListDirForTab: async () => new Promise((resolve) => pending.push(resolve)),
627 SearchFileRefsForTab: async () => [],
628 });
629 const { root, rerender } = await renderApproval({ workspaceScopeKey: "session-a" });
630
631 await clickImmediateAction("Revise plan");
632 await rerender({ insertRequest: { id: 20, text: "inspect @" } });
633 await waitFor("initial approval session scope request", () => pending.length === 1);
634 await rerender({ workspaceScopeKey: "session-b" });
635 await waitFor("next approval session scope request", () => pending.length === 2);
636
637 await act(async () => {
638 pending[1]([{ name: "current-plan-file.ts", isDir: false }]);
639 await flushTimers();
640 });
641 await waitFor("current approval session file result", () => document.body.textContent?.includes("current-plan-file.ts") === true);
642
643 await act(async () => {
644 pending[0]([{ name: "stale-plan-file.ts", isDir: false }]);
645 await flushTimers();
646 });
647 ok(document.body.textContent?.includes("current-plan-file.ts") === true, "current approval session file refs stay visible");
648 ok(document.body.textContent?.includes("stale-plan-file.ts") === false, "late approval session file refs are ignored");
649
650 await act(async () => {
651 root.unmount();
652 });
653 dom.window.close();
654 }
655
656 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
657 if (failed > 0) process.exit(1);
658
658 lines Plain Text