Skip to content

Commit d005bbd

Browse files
authored
Add controlling terminal coverage (#21)
* Add controlling terminal coverage * Fix terminal coverage test types
1 parent b72992c commit d005bbd

2 files changed

Lines changed: 81 additions & 6 deletions

File tree

src/core/terminal.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,26 @@ export interface ControllingTerminal {
2929
close: () => void;
3030
}
3131

32+
/** Minimal terminal construction hooks so tests can cover `/dev/tty` attach behavior. */
33+
export interface ControllingTerminalDeps {
34+
openSync: typeof fs.openSync;
35+
createReadStream: (fd: number) => tty.ReadStream;
36+
createWriteStream: (fd: number) => tty.WriteStream;
37+
}
38+
3239
/** Open the controlling terminal so the UI can stay interactive while stdin carries patch data. */
33-
export function openControllingTerminal(): ControllingTerminal | null {
40+
export function openControllingTerminal(
41+
deps: ControllingTerminalDeps = {
42+
openSync: fs.openSync,
43+
createReadStream: (fd) => new tty.ReadStream(fd),
44+
createWriteStream: (fd) => new tty.WriteStream(fd),
45+
},
46+
): ControllingTerminal | null {
3447
try {
35-
const stdinFd = fs.openSync("/dev/tty", "r");
36-
const stdoutFd = fs.openSync("/dev/tty", "w");
37-
const stdin = new tty.ReadStream(stdinFd);
38-
const stdout = new tty.WriteStream(stdoutFd);
48+
const stdinFd = deps.openSync("/dev/tty", "r");
49+
const stdoutFd = deps.openSync("/dev/tty", "w");
50+
const stdin = deps.createReadStream(stdinFd);
51+
const stdout = deps.createWriteStream(stdoutFd);
3952

4053
return {
4154
stdin,

test/terminal.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test";
22
import type { CliInput } from "../src/core/types";
3-
import { resolveRuntimeCliInput, shouldUsePagerMode, usesPipedPatchInput } from "../src/core/terminal";
3+
import { openControllingTerminal, resolveRuntimeCliInput, shouldUsePagerMode, usesPipedPatchInput } from "../src/core/terminal";
44

55
function createPatchInput(file?: string, pager = false): CliInput {
66
return {
@@ -35,3 +35,65 @@ describe("terminal runtime defaults", () => {
3535
expect(resolveRuntimeCliInput(input, true).options.pager).toBe(true);
3636
});
3737
});
38+
39+
describe("controlling terminal attachment", () => {
40+
test("opens /dev/tty for read and write and closes both streams", () => {
41+
const calls: Array<[string, string]> = [];
42+
let stdinDestroyed = false;
43+
let stdoutDestroyed = false;
44+
45+
const stdin = {
46+
destroy() {
47+
stdinDestroyed = true;
48+
},
49+
} as never;
50+
const stdout = {
51+
destroy() {
52+
stdoutDestroyed = true;
53+
},
54+
} as never;
55+
56+
const controllingTerminal = openControllingTerminal({
57+
openSync(path, flags) {
58+
calls.push([String(path), String(flags)]);
59+
return flags === "r" ? 11 : 12;
60+
},
61+
createReadStream(fd) {
62+
expect(fd).toBe(11);
63+
return stdin;
64+
},
65+
createWriteStream(fd) {
66+
expect(fd).toBe(12);
67+
return stdout;
68+
},
69+
});
70+
71+
expect(controllingTerminal).not.toBeNull();
72+
expect(calls).toEqual([
73+
["/dev/tty", "r"],
74+
["/dev/tty", "w"],
75+
]);
76+
expect(controllingTerminal?.stdin).toBe(stdin);
77+
expect(controllingTerminal?.stdout).toBe(stdout);
78+
79+
controllingTerminal?.close();
80+
expect(stdinDestroyed).toBe(true);
81+
expect(stdoutDestroyed).toBe(true);
82+
});
83+
84+
test("returns null when the controlling terminal cannot be opened", () => {
85+
const controllingTerminal = openControllingTerminal({
86+
openSync() {
87+
throw new Error("no tty");
88+
},
89+
createReadStream() {
90+
throw new Error("unreachable");
91+
},
92+
createWriteStream() {
93+
throw new Error("unreachable");
94+
},
95+
});
96+
97+
expect(controllingTerminal).toBeNull();
98+
});
99+
});

0 commit comments

Comments
 (0)