1import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
2import { act, type ReactNode } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { expect, it, vi } from "vite-plus/test";
5
6vi.mock("../ui/collapsible", () => {
7 const Children = ({ children }: { children: ReactNode }) => <>{children}</>;
8 return {
9 Collapsible: Children,
10 CollapsiblePanel: Children,
11 CollapsibleTrigger: Children,
12 };
13});
14
15vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> }));
16
17import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
18
19const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
20
21it("cancels an answer's pending auto-submit on revocation and resumes after a new grant", async () => {
22 vi.useFakeTimers();
23 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
24 vi.stubGlobal("window", {
25 setTimeout: globalThis.setTimeout,
26 clearTimeout: globalThis.clearTimeout,
27 });
28 vi.stubGlobal("document", { addEventListener: vi.fn(), removeEventListener: vi.fn() });
29 const onToggleOption = vi.fn();
30 const onAdvance = vi.fn();
31 const onDismiss = vi.fn();
32 const prompt = {
33 requestId: RuntimeRequestId.make("scoped-answer"),
34 createdAt: "2026-09-05T00:00:00.000Z",
35 dismissible: false,
36 responseCapability: "live" as const,
37 questions: [
38 {
39 id: "approach",
40 header: "Approach",
41 question: "Which approach?",
42 options: [{ label: "Incremental", description: "One module at a time" }],
43 multiSelect: false,
44 },
45 ],
46 };
47 const panel = (disabled: boolean) => (
48 <ComposerPendingUserInputPanel
49 pendingUserInputs={[prompt]}
50 cwd="/repo"
51 threadRef={threadRef}
52 disabled={disabled}
53 respondingRequestIds={[]}
54 answers={{}}
55 questionIndex={0}
56 onToggleOption={onToggleOption}
57 onAdvance={onAdvance}
58 onDismiss={onDismiss}
59 />
60 );
61 let renderer: ReactTestRenderer | undefined;
62 const chooseOption = () => {
63 const option = renderer!.root
64 .findAllByType("button")
65 .find((button) =>
66 button.findAllByType("span").some((span) => span.children.includes("Incremental")),
67 );
68 expect(option).toBeDefined();
69 option!.props.onClick();
70 };
71 try {
72 await act(async () => {
73 renderer = create(panel(false));
74 });
75 await act(async () => chooseOption());
76 expect(onToggleOption).toHaveBeenCalledWith("approach", "Incremental");
77 expect(onAdvance).not.toHaveBeenCalled();
78
79 await act(async () => renderer!.update(panel(true)));
80 await act(async () => {
81 chooseOption();
82 vi.runAllTimers();
83 });
84 expect(onToggleOption).toHaveBeenCalledTimes(1);
85 expect(onAdvance).not.toHaveBeenCalled();
86
87 await act(async () => renderer!.update(panel(false)));
88 await act(async () => {
89 chooseOption();
90 vi.runAllTimers();
91 });
92 expect(onToggleOption).toHaveBeenCalledTimes(2);
93 expect(onAdvance).toHaveBeenCalledTimes(1);
94 } finally {
95 await act(async () => renderer?.unmount());
96 vi.unstubAllGlobals();
97 vi.useRealTimers();
98 }
99});