This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/packages/bin/src/end_to_end_test.ts
alexion ab309d3226 feat: add kitchen view command and parser skeleton (task 0003)
Cut the first vertical slice through both layers: a runnable
`kitchen view <file>` that reads a Recipe File, parses it, and renders
it styled to the terminal.

@kitchen-md/core exposes a pure, total `parse(input): DocumentAST` built
on a minimal remark pipeline (parse + frontmatter) with a translation
layer to core's own AST types — frontmatter passthrough, HeadingBlock,
ParagraphBlock, and TextNode. remark types never leak into the public
API.

@kitchen-md/bin's `view` subcommand (commander) reads the file and hands
the DocumentAST to a pure `render(ast): string` (chalk, ANSI
auto-suppressed off a TTY). Fallible file I/O is modelled as a neverthrow
Result over a tagged-union CliError, matched at the boundary: stdout on
success, stderr and exit 1 on failure. The command functions sit beside
the import.meta.main-guarded CLI entry so they are testable in-process.

Tests follow ADR 0009's tiers — unit (parse, render), integration (the
view command's Result), and e2e (the binary as a subprocess) — with
coverage, a path-scoped test-report generator, and a prose fixture
rounding out the tooling. ADR 0008 records errors-as-values at the CLI
boundary; ADR 0009 records the testing tiers.
2026-07-28 23:31:03 -04:00

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");
});
});