fix(test): guard tui pty watch cli args

This commit is contained in:
Vincent Koc
2026-06-20 02:19:53 +02:00
parent 56c0405018
commit e02bee6aab
2 changed files with 94 additions and 7 deletions
+54 -7
View File
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
type Options = {
altScreen: boolean;
help: boolean;
mirrorPath: string;
mode: "fake" | "local" | "all";
vitestArgs: string[];
@@ -26,6 +27,12 @@ const CHILD_SIGTERM_GRACE_MS = 500;
const CHILD_SIGKILL_GRACE_MS = 5_000;
const MIRROR_READ_CHUNK_BYTES = 1024 * 1024;
const CHILD_OUTPUT_TAIL_BYTES = 128 * 1024;
const BOOLEAN_OPTIONS = new Set(["--help", "-h", "--no-alt-screen"]);
const VALUE_OPTIONS = new Set(["--mode", "--mirror-path"]);
class CliArgumentError extends Error {
override name = "CliArgumentError";
}
type KillableChild = {
pid?: number;
@@ -48,7 +55,11 @@ function readOption(args: string[], name: string): string | undefined {
if (idx < 0) {
return undefined;
}
return args[idx + 1]?.trim() || undefined;
const value = args[idx + 1];
if (!value || value.startsWith("--")) {
throw new CliArgumentError(`${name} requires a value`);
}
return value.trim();
}
function readMode(args: string[]): Options["mode"] {
@@ -56,20 +67,46 @@ function readMode(args: string[]): Options["mode"] {
if (mode === "fake" || mode === "local" || mode === "all") {
return mode;
}
throw new Error(`--mode must be fake, local, or all; got ${JSON.stringify(mode)}`);
throw new CliArgumentError(`--mode must be fake, local, or all; got ${JSON.stringify(mode)}`);
}
function usage(): string {
return [
"Usage: node --import tsx scripts/dev/tui-pty-test-watch.ts [options] [-- vitest args...]",
"",
"Options:",
" --mode <fake|local|all> Select TUI PTY test group (default: fake)",
" --mirror-path <path> Write/read mirrored ANSI output at this path",
" --no-alt-screen Print without switching to the terminal alt screen",
" -h, --help Show this help",
].join("\n");
}
function validateOwnArgs(args: string[]): void {
for (let idx = 0; idx < args.length; idx += 1) {
const arg = args[idx] ?? "";
if (BOOLEAN_OPTIONS.has(arg)) {
continue;
}
if (VALUE_OPTIONS.has(arg)) {
idx += 1;
continue;
}
throw new CliArgumentError(`Unknown argument: ${arg}`);
}
}
function parseOptions(args = process.argv.slice(2)): Options {
const separator = args.indexOf("--");
const ownArgs = separator >= 0 ? args.slice(0, separator) : args;
const vitestArgs = separator >= 0 ? args.slice(separator + 1) : [];
const mirrorPath =
readOption(ownArgs, "--mirror-path") !== undefined
? path.resolve(readOption(ownArgs, "--mirror-path") ?? "")
: DEFAULT_MIRROR_PATH;
validateOwnArgs(ownArgs);
const mirrorPathOption = readOption(ownArgs, "--mirror-path");
return {
altScreen: !ownArgs.includes("--no-alt-screen"),
mirrorPath,
help: ownArgs.includes("--help") || ownArgs.includes("-h"),
mirrorPath:
mirrorPathOption !== undefined ? path.resolve(mirrorPathOption) : DEFAULT_MIRROR_PATH,
mode: readMode(ownArgs),
vitestArgs,
};
@@ -209,6 +246,10 @@ async function drainNewMirrorData(
async function main(): Promise<void> {
const options = parseOptions();
if (options.help) {
process.stdout.write(`${usage()}\n`);
return;
}
const useAltScreen = shouldUseAltScreen(options);
await createMirrorFile(options.mirrorPath);
@@ -422,6 +463,10 @@ async function main(): Promise<void> {
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main().catch((error: unknown) => {
if (error instanceof CliArgumentError) {
process.stderr.write(`${error.message}\n`);
process.exit(1);
}
process.stderr.write(
`${error instanceof Error ? error.stack || error.message : String(error)}\n`,
);
@@ -433,6 +478,8 @@ export const testing = {
appendBufferTail,
createChildStopper,
drainNewMirrorData,
parseOptions,
readNewMirrorData,
signalChildProcessTree,
usage,
};
+40
View File
@@ -1,4 +1,5 @@
// Dev Tooling Safety tests cover dev tooling safety script behavior.
import { spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import os from "node:os";
@@ -205,6 +206,45 @@ describe("script-specific dev tooling hardening", () => {
expect(calls).toBe(1);
});
it("prints TUI PTY watch usage without launching the watcher", () => {
const result = spawnSync(
process.execPath,
["--import", "tsx", "scripts/dev/tui-pty-test-watch.ts", "--help"],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage: node --import tsx scripts/dev/tui-pty-test-watch.ts");
expect(result.stderr).toBe("");
});
it("rejects unknown TUI PTY watch args before launching the watcher", () => {
expect(() => tuiPtyWatchTesting.parseOptions(["--wat"])).toThrow("Unknown argument: --wat");
const result = spawnSync(
process.execPath,
["--import", "tsx", "scripts/dev/tui-pty-test-watch.ts", "--wat"],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(1);
expect(result.stderr.trim()).toBe("Unknown argument: --wat");
expect(result.stdout).toBe("");
});
it("keeps TUI PTY watch vitest args behind the separator", () => {
expect(tuiPtyWatchTesting.parseOptions(["--mode", "all", "--", "--help"])).toMatchObject({
mode: "all",
vitestArgs: ["--help"],
});
});
it("escalates stalled TUI PTY watch children after interrupt cleanup", async () => {
vi.useFakeTimers();
const signals: NodeJS.Signals[] = [];