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