From f36fb0daa1cbf9f3ac8d679d4908963dab36125d Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sun, 22 Mar 2026 09:26:43 -0400 Subject: [PATCH 1/2] Add controlling terminal coverage --- src/core/terminal.ts | 23 ++++++++++++---- test/terminal.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/core/terminal.ts b/src/core/terminal.ts index 11870961..5e99a143 100644 --- a/src/core/terminal.ts +++ b/src/core/terminal.ts @@ -29,13 +29,26 @@ export interface ControllingTerminal { close: () => void; } +/** Minimal terminal construction hooks so tests can cover `/dev/tty` attach behavior. */ +export interface ControllingTerminalDeps { + openSync: typeof fs.openSync; + createReadStream: (fd: number) => tty.ReadStream; + createWriteStream: (fd: number) => tty.WriteStream; +} + /** Open the controlling terminal so the UI can stay interactive while stdin carries patch data. */ -export function openControllingTerminal(): ControllingTerminal | null { +export function openControllingTerminal( + deps: ControllingTerminalDeps = { + openSync: fs.openSync, + createReadStream: (fd) => new tty.ReadStream(fd), + createWriteStream: (fd) => new tty.WriteStream(fd), + }, +): ControllingTerminal | null { try { - const stdinFd = fs.openSync("/dev/tty", "r"); - const stdoutFd = fs.openSync("/dev/tty", "w"); - const stdin = new tty.ReadStream(stdinFd); - const stdout = new tty.WriteStream(stdoutFd); + const stdinFd = deps.openSync("/dev/tty", "r"); + const stdoutFd = deps.openSync("/dev/tty", "w"); + const stdin = deps.createReadStream(stdinFd); + const stdout = deps.createWriteStream(stdoutFd); return { stdin, diff --git a/test/terminal.test.ts b/test/terminal.test.ts index 11169a4a..b5f29ac0 100644 --- a/test/terminal.test.ts +++ b/test/terminal.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import type { CliInput } from "../src/core/types"; -import { resolveRuntimeCliInput, shouldUsePagerMode, usesPipedPatchInput } from "../src/core/terminal"; +import { openControllingTerminal, resolveRuntimeCliInput, shouldUsePagerMode, usesPipedPatchInput } from "../src/core/terminal"; function createPatchInput(file?: string, pager = false): CliInput { return { @@ -35,3 +35,65 @@ describe("terminal runtime defaults", () => { expect(resolveRuntimeCliInput(input, true).options.pager).toBe(true); }); }); + +describe("controlling terminal attachment", () => { + test("opens /dev/tty for read and write and closes both streams", () => { + const calls: Array<[string, string]> = []; + let stdinDestroyed = false; + let stdoutDestroyed = false; + + const stdin = { + destroy() { + stdinDestroyed = true; + }, + } as never; + const stdout = { + destroy() { + stdoutDestroyed = true; + }, + } as never; + + const controllingTerminal = openControllingTerminal({ + openSync(path, flags) { + calls.push([path, flags]); + return flags === "r" ? 11 : 12; + }, + createReadStream(fd) { + expect(fd).toBe(11); + return stdin; + }, + createWriteStream(fd) { + expect(fd).toBe(12); + return stdout; + }, + }); + + expect(controllingTerminal).not.toBeNull(); + expect(calls).toEqual([ + ["/dev/tty", "r"], + ["/dev/tty", "w"], + ]); + expect(controllingTerminal?.stdin).toBe(stdin); + expect(controllingTerminal?.stdout).toBe(stdout); + + controllingTerminal?.close(); + expect(stdinDestroyed).toBe(true); + expect(stdoutDestroyed).toBe(true); + }); + + test("returns null when the controlling terminal cannot be opened", () => { + const controllingTerminal = openControllingTerminal({ + openSync() { + throw new Error("no tty"); + }, + createReadStream() { + throw new Error("unreachable"); + }, + createWriteStream() { + throw new Error("unreachable"); + }, + }); + + expect(controllingTerminal).toBeNull(); + }); +}); From 2cc772d2a62f719f980ac2f2c443578a97c192d2 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sun, 22 Mar 2026 09:48:49 -0400 Subject: [PATCH 2/2] Fix terminal coverage test types --- test/terminal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/terminal.test.ts b/test/terminal.test.ts index b5f29ac0..eceb3e91 100644 --- a/test/terminal.test.ts +++ b/test/terminal.test.ts @@ -55,7 +55,7 @@ describe("controlling terminal attachment", () => { const controllingTerminal = openControllingTerminal({ openSync(path, flags) { - calls.push([path, flags]); + calls.push([String(path), String(flags)]); return flags === "r" ? 11 : 12; }, createReadStream(fd) {