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.
This commit was merged in pull request #2.
This commit is contained in:
65
packages/bin/src/end_to_end_test.ts
Normal file
65
packages/bin/src/end_to_end_test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user