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