Guard the CLI entry's program.parse() behind import.meta.main so the command module can be imported without running the CLI, then rebuild the deleted suite across the three tiers ADR 0009 defines: - unit: parse (core) and render (bin), asserted on return values - integration: the view command function, asserted on its Result - e2e: the kitchen binary as a subprocess (exit codes, streams, ANSI suppression, errors, --help) Drop the stale test-support.ts coverage ignore and refresh the task's implementation notes to describe the tiered suite.
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { join } from "node:path";
|
|
|
|
const repoRoot = join(import.meta.dir, "..", "..", "..");
|
|
const entry = join(repoRoot, "packages", "bin", "src", "index.ts");
|
|
|
|
async function runKitchen(args: string[]) {
|
|
const proc = Bun.spawn(["bun", entry, ...args], {
|
|
cwd: repoRoot,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
]);
|
|
const exitCode = await proc.exited;
|
|
return { exitCode, stdout, stderr };
|
|
}
|
|
|
|
describe("kitchen CLI (e2e)", () => {
|
|
test("view renders a recipe file to stdout and exits 0", async () => {
|
|
const { exitCode, stdout, stderr } = await runKitchen(["view", "fixtures/prose.md"]);
|
|
const out = Bun.stripANSI(stdout);
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
expect(out).toContain("Buttered Toast");
|
|
expect(out).toContain("Method");
|
|
expect(out).toContain("Serving");
|
|
expect(out).toContain("Cut into triangles and serve at once.");
|
|
});
|
|
|
|
test("view preserves document order in the output", async () => {
|
|
const { stdout } = await runKitchen(["view", "fixtures/prose.md"]);
|
|
const out = Bun.stripANSI(stdout);
|
|
expect(out.indexOf("Method")).toBeLessThan(out.indexOf("Serving"));
|
|
});
|
|
|
|
test("ANSI styling is suppressed when stdout is not a TTY (piped)", async () => {
|
|
const { stdout } = await runKitchen(["view", "fixtures/prose.md"]);
|
|
expect(stdout).not.toContain("\x1b");
|
|
});
|
|
|
|
test("a nonexistent file prints a human-readable error to stderr and exits 1", async () => {
|
|
const { exitCode, stdout, stderr } = await runKitchen(["view", "does/not/exist.md"]);
|
|
expect(exitCode).toBe(1);
|
|
expect(stdout).toBe("");
|
|
expect(stderr).toContain("cannot read");
|
|
expect(stderr).toContain("does/not/exist.md");
|
|
expect(stderr.startsWith("kitchen:")).toBe(true);
|
|
});
|
|
|
|
test("a missing file argument prints usage to stderr and exits 1", async () => {
|
|
const { exitCode, stderr } = await runKitchen(["view"]);
|
|
expect(exitCode).toBe(1);
|
|
expect(stderr).not.toBe("");
|
|
expect(stderr.toLowerCase()).toContain("missing required argument");
|
|
});
|
|
|
|
test("--help lists the view command and exits 0", async () => {
|
|
const { exitCode, stdout } = await runKitchen(["--help"]);
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toContain("view");
|
|
});
|
|
});
|