From 86eba1a6242b547f45768d5478e7502cd8eeb515 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 10 Mar 2026 09:45:43 -0700 Subject: [PATCH] docs: add @env/@setup directives to CLAUDE.md --- CLAUDE.md | 4 +++- src/cli/index.ts | 34 ++++++++++++++++------------------ src/parse.test.ts | 6 ++++++ src/parse.ts | 2 ++ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9bf54dc..20ab1e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Transcript-based shell integration test runner. Bun + TypeScript. - `bun test` — run unit tests - `bunx tsc --noEmit` — type check -- `bun run src/cli/index.ts [files...]` — run shout CLI +- `bun run src/cli/index.ts [files...]` — run shout CLI (`--port-from ` auto-assigns `$PORT`) ## Architecture @@ -28,6 +28,8 @@ Transcript-based shell integration test runner. Bun + TypeScript. - `[N]` on last line of expected output = assert exit code N - `[*]` = assert any non-zero exit code; default expects 0 - `#` after a command = comment (stripped); `#` in expected output is literal +- `@env KEY=VALUE` before first command = set environment variable +- `@setup path.shout` before first command = prepend commands (and `@env`) from another file - Each file runs in a fresh temp dir with a single `/bin/sh` session ## Style diff --git a/src/cli/index.ts b/src/cli/index.ts index a357e30..292dbee 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -5,7 +5,7 @@ import { resolve, relative, dirname } from "node:path" import { program } from "commander" import ansis from "ansis" -import { parse, type ShoutFile } from "../parse.ts" +import { parse, type Command, type ShoutFile } from "../parse.ts" import { runFile, cleanupTmpDir } from "../run.ts" import { evaluateFile, formatFailure, formatSummary } from "../format.ts" import type { TestResult } from "../format.ts" @@ -103,21 +103,19 @@ $ true if (d.type === "env") envVars[d.key] = d.value } - // Resolve @setup directives: prepend setup commands - let setupCount = 0 - let merged: ShoutFile = parsed - const setupDirectives = parsed.directives.filter(d => d.type === "setup") - if (setupDirectives.length > 0) { - const setupCommands = [] - for (const d of setupDirectives) { - const setupPath = resolve(dirname(filePath), d.path) - const setupContent = await readFile(setupPath, "utf-8") - const setupParsed = parse(relative(cwd, setupPath), setupContent) - setupCommands.push(...setupParsed.commands) + // Resolve @setup directives: prepend setup commands, collect their @env + const setupCommands: Command[] = [] + for (const d of parsed.directives) { + if (d.type !== "setup") continue + const setupPath = resolve(dirname(filePath), d.path) + const setupContent = await readFile(setupPath, "utf-8") + const setupParsed = parse(relative(cwd, setupPath), setupContent) + for (const sd of setupParsed.directives) { + if (sd.type === "env") envVars[sd.key] = sd.value } - setupCount = setupCommands.length - merged = { ...parsed, commands: [...setupCommands, ...parsed.commands] } + setupCommands.push(...setupParsed.commands) } + const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands] } const fileResult = await runFile(merged, { cleanEnv: opts.cleanEnv ?? false, @@ -130,15 +128,15 @@ $ true : undefined, }) + const fileOwnResults = fileResult.results.slice(setupCommands.length) + const testResult = evaluateFile( parsed.path, - fileResult.results, + fileOwnResults, fileResult.error, ) - if (opts.update && fileResult.results.length > 0) { - // Only rewrite with the file's own results, not setup results - const fileOwnResults = fileResult.results.slice(setupCount) + if (opts.update && fileOwnResults.length > 0) { const updated = rewriteFile(parsed, fileOwnResults, content) if (updated !== content) { await writeFile(filePath, updated) diff --git a/src/parse.test.ts b/src/parse.test.ts index 8af05ba..70a28f7 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -109,4 +109,10 @@ describe("parse", () => { const result = parse("test.shout", "$ echo hi\nhi\n") expect(result.directives).toEqual([]) }) + + test("unknown directive throws", () => { + expect(() => parse("test.shout", "@evn PORT=3000\n$ echo hi\n")).toThrow( + "test.shout:1: unknown directive: @evn PORT=3000", + ) + }) }) diff --git a/src/parse.ts b/src/parse.ts index bd62914..4bd07a4 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -79,6 +79,8 @@ export function parse(path: string, content: string): ShoutFile { if (eq > 0) { directives.push({ type: "env", key: rest.slice(0, eq), value: rest.slice(eq + 1), line: i + 1 }) } + } else { + throw new Error(`${path}:${i + 1}: unknown directive: ${line}`) } continue }