1import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
3const io = vi.hoisted(() => ({
4 platform: vi.fn(() => "win32"),
5 stat: vi.fn(() => ({ isCharacterDevice: (): boolean => true })),
6 open: vi.fn(() => 42),
7 close: vi.fn(),
8 isatty: vi.fn((fd: number) => fd !== 0),
9}));
10
11vi.mock("node:fs", () => ({ fstatSync: io.stat, openSync: io.open, closeSync: io.close }));
12vi.mock("node:tty", async () => {
13 const { EventEmitter } = await import("node:events");
14 return {
15 isatty: io.isatty,
16 ReadStream: class extends EventEmitter {
17 readonly isTTY = true;
18 readonly _readableState = { reading: true };
19 _handle: { reading: boolean; readStop: () => void } | undefined = {
20 reading: true,
21 readStop: vi.fn(),
22 };
23 },
24 };
25});
26vi.mock("@t3tools/shared/HostProcess", () => ({ Platform: { defaultValue: io.platform } }));
27
28const originalStdin = Object.getOwnPropertyDescriptor(process, "stdin")!;
29
30beforeEach(() => {
31 vi.resetModules();
32 vi.resetAllMocks();
33 vi.stubEnv("T3CODE_DESKTOP_CLI_LAUNCHER", "1");
34});
35
36afterEach(() => {
37 Object.defineProperty(process, "stdin", originalStdin);
38 vi.unstubAllEnvs();
39});
40
41describe("attachConsoleStdin", () => {
42 it("reopens the console when Node replaces missing Windows stdin with NUL", async () => {
43 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
44
45 expect(attachConsoleStdin()).toBe(42);
46 expect(process.stdin.isTTY).toBe(true);
47 expect(interactiveStdin()).toBe(42);
48 expect(process.env.T3CODE_DESKTOP_CLI_LAUNCHER).toBeUndefined();
49 });
50
51 it("releases console reads when the picker pauses stdin before launching an agent", async () => {
52 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
53 attachConsoleStdin();
54 const stdin = process.stdin as unknown as {
55 emit(event: string): boolean;
56 _readableState: { reading: boolean };
57 _handle: { reading: boolean; readStop: () => void };
58 };
59 stdin.emit("pause");
60 expect(stdin._readableState.reading).toBe(false);
61 expect(stdin._handle.reading).toBe(false);
62 expect(stdin._handle.readStop).toHaveBeenCalledOnce();
63 expect(interactiveStdin()).toBe(42);
64 });
65
66 it("can pause after the reopened stream's handle has been closed", async () => {
67 const { attachConsoleStdin } = await import("./consoleStdin.ts");
68 attachConsoleStdin();
69 const stdin = process.stdin as unknown as {
70 emit(event: string): boolean;
71 _handle: undefined;
72 };
73 stdin._handle = undefined;
74 expect(() => stdin.emit("pause")).not.toThrow();
75 });
76
77 it("still reopens a closed stdin", async () => {
78 io.stat.mockImplementation(() => {
79 throw new Error("EBADF");
80 });
81 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
82 expect(attachConsoleStdin()).toBe(42);
83 expect(interactiveStdin()).toBe(42);
84 });
85
86 it.each(["pipe"])("preserves redirected %s input", async () => {
87 io.stat.mockReturnValue({ isCharacterDevice: () => false });
88 const stdin = process.stdin;
89 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
90 expect(attachConsoleStdin()).toBeUndefined();
91 expect(process.stdin).toBe(stdin);
92 expect(interactiveStdin()).toBe("inherit");
93 expect(io.open).not.toHaveBeenCalled();
94 });
95
96 it.each(["console input", "redirected output", "unmarked process", "non-Windows"])(
97 "leaves %s alone",
98 async (scenario) => {
99 if (scenario === "console input") io.isatty.mockReturnValue(true);
100 if (scenario === "redirected output") io.isatty.mockReturnValue(false);
101 if (scenario === "unmarked process") delete process.env.T3CODE_DESKTOP_CLI_LAUNCHER;
102 if (scenario === "non-Windows") io.platform.mockReturnValue("darwin");
103 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
104 expect(attachConsoleStdin()).toBeUndefined();
105 expect(interactiveStdin()).toBe("inherit");
106 expect(io.open).not.toHaveBeenCalled();
107 expect(process.env.T3CODE_DESKTOP_CLI_LAUNCHER).toBeUndefined();
108 },
109 );
110
111 it("keeps inherited input when CONIN$ cannot be opened", async () => {
112 io.open.mockImplementation(() => {
113 throw new Error("ENOENT");
114 });
115 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
116 expect(attachConsoleStdin()).toBeUndefined();
117 expect(interactiveStdin()).toBe("inherit");
118 });
119
120 it("closes a reopened descriptor that is not a console", async () => {
121 io.isatty.mockImplementation((fd) => fd === 1);
122 const { attachConsoleStdin, interactiveStdin } = await import("./consoleStdin.ts");
123 expect(attachConsoleStdin()).toBeUndefined();
124 expect(interactiveStdin()).toBe("inherit");
125 expect(io.close).toHaveBeenCalledWith(42);
126 });
127});