@jterrazz/test 7.1.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["sep","grepUtil","postgres","redis"],"sources":["../src/builder/cli/adapters/exec.adapter.ts","../src/builder/http/adapters/fetch.adapter.ts","../src/builder/http/adapters/hono.adapter.ts","../src/builder/common/directory.ts","../src/builder/common/reporter.ts","../src/builder/common/directory-accessor.ts","../src/builder/common/grep.ts","../src/builder/common/response-accessor.ts","../src/builder/common/table-assertion.ts","../src/builder/common/result.ts","../src/builder/specification-builder.ts","../src/docker/docker-adapter.ts","../src/docker/docker-assertion.ts","../src/infra/adapters/compose.adapter.ts","../src/infra/adapters/testcontainers.adapter.ts","../src/infra/compose-parser.ts","../src/infra/orchestrator.ts","../src/spec/resolve.ts","../src/spec/spec.ts","../src/spec/targets.ts","../src/spec/legacy-cli.ts","../src/spec/legacy-e2e.ts","../src/spec/legacy-integration.ts"],"sourcesContent":["import { execSync, spawn } from 'node:child_process';\n\nimport type { CommandEnv, CommandPort, CommandResult, SpawnOptions } from '../command.port.js';\n\n/**\n * Build a child-process env from the parent env plus user overrides.\n * `null` overrides delete keys (e.g. `INIT_CWD: null`).\n */\nfunction buildEnv(extra?: CommandEnv): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env, INIT_CWD: undefined };\n if (extra) {\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) {\n delete env[key];\n } else {\n env[key] = value;\n }\n }\n }\n return env;\n}\n\n/**\n * Executes CLI commands via Node.js child_process.\n * Uses `execSync` for one-shot commands and `spawn` for long-running processes.\n * Used by the `cli()` specification runner.\n */\nexport class ExecAdapter implements CommandPort {\n private command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n\n async exec(args: string, cwd: string, extraEnv?: CommandEnv): Promise<CommandResult> {\n const env = buildEnv(extraEnv);\n\n try {\n const stdout = execSync(`${this.command} ${args}`, {\n cwd,\n encoding: 'utf8',\n env,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return { exitCode: 0, stdout, stderr: '' };\n } catch (error: any) {\n return {\n exitCode: error.status ?? 1,\n stdout: error.stdout?.toString() ?? '',\n stderr: error.stderr?.toString() ?? '',\n };\n }\n }\n\n async spawn(\n args: string,\n cwd: string,\n options: SpawnOptions,\n extraEnv?: CommandEnv,\n ): Promise<CommandResult> {\n const env = buildEnv(extraEnv);\n\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let resolved = false;\n\n const child = spawn(this.command, args.split(/\\s+/).filter(Boolean), {\n cwd,\n env,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n const finish = (exitCode: number) => {\n if (resolved) {\n return;\n }\n resolved = true;\n child.kill('SIGTERM');\n resolve({ exitCode, stdout, stderr });\n };\n\n let patternMatched = false;\n\n const checkPattern = () => {\n if (\n !patternMatched &&\n (stdout.includes(options.waitFor) || stderr.includes(options.waitFor))\n ) {\n patternMatched = true;\n finish(0);\n }\n };\n\n child.stdout?.on('data', (data: Buffer) => {\n stdout += data.toString();\n checkPattern();\n });\n\n child.stderr?.on('data', (data: Buffer) => {\n stderr += data.toString();\n checkPattern();\n });\n\n // Process exited before pattern matched\n child.on('exit', (code) => {\n if (!patternMatched) {\n finish(code === 0 ? 1 : (code ?? 1));\n }\n });\n\n setTimeout(() => finish(124), options.timeout);\n });\n }\n}\n","import type { ServerPort, ServerResponse } from '../server.port.js';\n\n/**\n * Server adapter that sends real HTTP requests via the Fetch API.\n * Used by the `e2e()` specification runner to hit a live server.\n */\nexport class FetchAdapter implements ServerPort {\n private baseUrl: string;\n\n constructor(url: string) {\n this.baseUrl = url.replace(/\\/$/, '');\n }\n\n async request(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record<string, string>,\n ): Promise<ServerResponse> {\n const init: RequestInit = {\n method,\n headers: { 'Content-Type': 'application/json', ...headers },\n };\n\n if (body !== undefined) {\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(`${this.baseUrl}${path}`, init);\n const responseBody = await response.json().catch(() => null);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n status: response.status,\n body: responseBody,\n headers: responseHeaders,\n };\n }\n}\n","import type { ServerPort, ServerResponse } from '../server.port.js';\n\n/**\n * Server adapter that dispatches requests in-process through a Hono app instance.\n * Used by the `integration()` specification runner -- no network overhead.\n */\nexport class HonoAdapter implements ServerPort {\n private app: {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n };\n\n constructor(app: {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n }) {\n this.app = app;\n }\n\n async request(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record<string, string>,\n ): Promise<ServerResponse> {\n const init: RequestInit = {\n method,\n headers: { 'Content-Type': 'application/json', ...headers },\n };\n\n if (body !== undefined) {\n init.body = JSON.stringify(body);\n }\n\n const response = await this.app.request(path, init);\n const responseBody = await response.json().catch(() => null);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n status: response.status,\n body: responseBody,\n headers: responseHeaders,\n };\n }\n}\n","import { readFileSync, statSync } from 'node:fs';\nimport { readdir } from 'node:fs/promises';\nimport { relative, resolve, sep } from 'node:path';\n\n/**\n * Default ignore patterns — paths that should never appear in a tracked snapshot.\n * Each entry is matched against any path segment OR a path prefix.\n */\nconst DEFAULT_IGNORES = ['.git', '.DS_Store', 'node_modules', '.next', 'dist', '.turbo', '.cache'];\n\nexport interface DirectoryDiff {\n added: string[];\n changed: { path: string; expected: string; actual: string }[];\n removed: string[];\n}\n\ninterface WalkOptions {\n ignore?: string[];\n}\n\n/**\n * Recursively walk a directory, returning sorted relative paths of files only.\n * Ignored entries (default + caller-supplied) are skipped.\n */\nexport async function walkDirectory(root: string, options: WalkOptions = {}): Promise<string[]> {\n const ignores = new Set([...DEFAULT_IGNORES, ...(options.ignore ?? [])]);\n const out: string[] = [];\n\n async function walk(current: string): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(current);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (ignores.has(entry)) {\n continue;\n }\n const abs = resolve(current, entry);\n const stat = statSync(abs);\n if (stat.isDirectory()) {\n await walk(abs);\n } else if (stat.isFile()) {\n out.push(relative(root, abs).split(sep).join('/'));\n }\n }\n }\n\n await walk(root);\n out.sort();\n return out;\n}\n\n/**\n * Compare two directory trees file-by-file.\n * Binary files are compared by byte equality but reported without inline diff.\n */\nexport async function diffDirectories(\n expectedRoot: string,\n actualRoot: string,\n options: WalkOptions = {},\n): Promise<DirectoryDiff> {\n const expectedFiles = await walkDirectory(expectedRoot, options);\n const actualFiles = await walkDirectory(actualRoot, options);\n\n const expectedSet = new Set(expectedFiles);\n const actualSet = new Set(actualFiles);\n\n const added = actualFiles.filter((f) => !expectedSet.has(f));\n const removed = expectedFiles.filter((f) => !actualSet.has(f));\n const changed: DirectoryDiff['changed'] = [];\n\n for (const file of expectedFiles) {\n if (!actualSet.has(file)) {\n continue;\n }\n const expected = readFileSync(resolve(expectedRoot, file), 'utf8');\n const actual = readFileSync(resolve(actualRoot, file), 'utf8');\n if (expected !== actual) {\n changed.push({ actual, expected, path: file });\n }\n }\n\n return { added, changed, removed };\n}\n","// ── Colors ──\n\nconst GREEN = '\\x1b[32m';\nconst RED = '\\x1b[31m';\nconst DIM = '\\x1b[2m';\nconst BOLD = '\\x1b[1m';\nconst RESET = '\\x1b[0m';\nconst BG_CYAN = '\\x1b[46m';\nconst BLACK = '\\x1b[30m';\n\n// ── Symbols (vitest-native) ──\n\nconst CHECK = '✓';\nconst CROSS = '×';\nconst ARROW = '→';\nconst DASH = '⎯';\n\n// ── Types ──\n\n/** Status report for a single infrastructure service after startup. */\nexport interface ServiceReport {\n name: string;\n type: string;\n connectionString?: string;\n durationMs: number;\n error?: string;\n logs?: string;\n}\n\n/** Describes the application under test for startup reporting. */\nexport interface AppInfo {\n type: 'http' | 'in-process';\n url?: string;\n}\n\n// ── Startup report ──\n\nexport function formatStartupReport(\n mode: 'e2e' | 'integration',\n services: ServiceReport[],\n app?: AppInfo,\n): string {\n const lines: string[] = [];\n\n lines.push('');\n lines.push(`${BG_CYAN}${BLACK}${BOLD} INFRA ${RESET} Starting infrastructure...`);\n lines.push('');\n\n for (const service of services) {\n if (service.error) {\n lines.push(\n ` ${RED}${CROSS}${RESET} ${service.type} (${service.name}) ${RED}${service.error}${RESET} ${DIM}${service.durationMs}ms${RESET}`,\n );\n if (service.logs) {\n const logLines = service.logs.trim().split('\\n').slice(-10);\n for (const logLine of logLines) {\n lines.push(` ${DIM}${logLine}${RESET}`);\n }\n }\n } else {\n const conn = service.connectionString\n ? `${DIM}${service.connectionString}${RESET}`\n : '';\n lines.push(\n ` ${GREEN}${CHECK}${RESET} ${service.type} (${service.name}) ${conn} ${DIM}${service.durationMs}ms${RESET}`,\n );\n }\n }\n\n if (app) {\n lines.push('');\n if (app.type === 'in-process') {\n lines.push(` ${DIM}${ARROW} app: in-process (Hono)${RESET}`);\n } else {\n lines.push(` ${DIM}${ARROW} app: ${app.url}${RESET}`);\n }\n }\n\n lines.push('');\n\n return lines.join('\\n');\n}\n\n// ── Error divider ──\n\nexport function formatErrorDivider(label: string): string {\n const dashes = DASH.repeat(30);\n return `\\n${RED}${dashes} ${label} ${dashes}${RESET}\\n`;\n}\n\n// ── Status error ──\n\nexport function formatStatusError(\n expectedStatus: number,\n receivedStatus: number,\n request: { method: string; path: string; body?: unknown },\n responseBody: unknown,\n): string {\n const lines: string[] = [];\n\n lines.push(`Expected status: ${GREEN}${expectedStatus}${RESET}`);\n lines.push(`Received status: ${RED}${receivedStatus}${RESET}`);\n lines.push('');\n lines.push(`${DIM}${request.method} ${request.path}${RESET}`);\n\n if (request.body) {\n lines.push(formatJson(request.body, DIM));\n }\n\n if (responseBody) {\n lines.push('');\n lines.push(`${DIM}Response:${RESET}`);\n lines.push(formatJson(responseBody, RED));\n }\n\n return lines.join('\\n');\n}\n\n// ── Table diff ──\n\nexport function formatTableDiff(\n table: string,\n columns: string[],\n expected: unknown[][],\n actual: unknown[][],\n): string {\n const lines: string[] = [];\n\n lines.push(`Table \"${table}\" mismatch`);\n lines.push(`${DIM} query: ${columns.join(', ')}${RESET}`);\n lines.push(`${DIM} expected: ${rowLabel(expected.length)}${RESET}`);\n lines.push(`${DIM} received: ${rowLabel(actual.length)}${RESET}`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const header = columns.join(' | ');\n lines.push(`${DIM} ${header}${RESET}`);\n\n const maxRows = Math.max(expected.length, actual.length);\n\n for (let i = 0; i < maxRows; i++) {\n const exp = expected[i];\n const act = actual[i];\n\n if (exp && !act) {\n lines.push(`${GREEN}- ${formatRow(exp)}${RESET}`);\n } else if (!exp && act) {\n lines.push(`${RED}+ ${formatRow(act)}${RESET}`);\n } else if (exp && act) {\n const same = JSON.stringify(exp) === JSON.stringify(act);\n if (same) {\n lines.push(` ${formatRow(act)}`);\n } else {\n lines.push(`${GREEN}- ${formatRow(exp)}${RESET}`);\n lines.push(`${RED}+ ${formatRow(act)}${RESET}`);\n }\n }\n }\n\n if (expected.length === 0 && actual.length === 0) {\n lines.push(` (empty)`);\n }\n\n return lines.join('\\n');\n}\n\n// ── Response diff ──\n\nexport function formatResponseDiff(file: string, expected: unknown, actual: unknown): string {\n const lines: string[] = [];\n\n lines.push(`Response mismatch (${file})`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const expectedLines = JSON.stringify(expected, null, 2).split('\\n');\n const actualLines = JSON.stringify(actual, null, 2).split('\\n');\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n\n if (exp === act) {\n lines.push(` ${exp}`);\n } else {\n if (exp !== undefined) {\n lines.push(`${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(`${RED}+ ${act}${RESET}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Exit code error ──\n\nexport function formatExitCodeError(\n expected: number,\n received: number,\n stdout: string,\n stderr: string,\n): string {\n const lines: string[] = [];\n\n lines.push(`Expected exit code: ${GREEN}${expected}${RESET}`);\n lines.push(`Received exit code: ${RED}${received}${RESET}`);\n\n if (stdout.trim()) {\n lines.push('');\n lines.push(`${DIM}stdout:${RESET}`);\n for (const line of stdout.trim().split('\\n').slice(-15)) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n }\n\n if (stderr.trim()) {\n lines.push('');\n lines.push(`${DIM}stderr:${RESET}`);\n for (const line of stderr.trim().split('\\n').slice(-15)) {\n lines.push(` ${RED}${line}${RESET}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Stdout/stderr diff ──\n\nexport function formatStdoutDiff(file: string, expected: string, actual: string): string {\n const lines: string[] = [];\n\n lines.push(`Output mismatch (${file})`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const expectedLines = expected.split('\\n');\n const actualLines = actual.split('\\n');\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n\n if (exp === act) {\n lines.push(` ${exp}`);\n } else {\n if (exp !== undefined) {\n lines.push(`${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(`${RED}+ ${act}${RESET}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Directory diff ──\n\ninterface DirectoryDiffData {\n added: string[];\n changed: { path: string; expected: string; actual: string }[];\n removed: string[];\n}\n\nexport function formatDirectoryDiff(\n fixtureName: string,\n diff: DirectoryDiffData,\n hint: string,\n): string {\n const lines: string[] = [];\n\n const total = diff.added.length + diff.removed.length + diff.changed.length;\n lines.push(`Directory mismatch: ${BOLD}${fixtureName}${RESET}`);\n lines.push(\n `${DIM} ${total} difference${total === 1 ? '' : 's'}: ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed${RESET}`,\n );\n lines.push('');\n lines.push(`${GREEN}- Expected (fixture)${RESET}`);\n lines.push(`${RED}+ Received (generated)${RESET}`);\n lines.push('');\n\n for (const path of diff.added) {\n lines.push(`${RED}+ added ${path}${RESET} ${DIM}(not in fixture)${RESET}`);\n }\n for (const path of diff.removed) {\n lines.push(`${GREEN}- removed ${path}${RESET} ${DIM}(in fixture, not generated)${RESET}`);\n }\n for (const { path, expected, actual } of diff.changed) {\n const expectedLines = expected.split('\\n');\n const actualLines = actual.split('\\n');\n const changedCount = countLineDifferences(expectedLines, actualLines);\n lines.push(\n `${BOLD}~ changed ${path}${RESET} ${DIM}(${changedCount} line${changedCount === 1 ? '' : 's'} differ)${RESET}`,\n );\n\n let shown = 0;\n const maxShown = 5;\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n for (let i = 0; i < maxLines && shown < maxShown; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n if (exp !== act) {\n lines.push(`${DIM} line ${i + 1}:${RESET}`);\n if (exp !== undefined) {\n lines.push(` ${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(` ${RED}+ ${act}${RESET}`);\n }\n shown++;\n }\n }\n if (changedCount > maxShown) {\n lines.push(` ${DIM}... ${changedCount - maxShown} more line(s)${RESET}`);\n }\n }\n\n lines.push('');\n lines.push(`${DIM}${hint}${RESET}`);\n\n return lines.join('\\n');\n}\n\nfunction countLineDifferences(expected: string[], actual: string[]): number {\n let count = 0;\n const max = Math.max(expected.length, actual.length);\n for (let i = 0; i < max; i++) {\n if (expected[i] !== actual[i]) {\n count++;\n }\n }\n return count;\n}\n\n// ── File assertions ──\n\nexport function formatFileMissing(path: string): string {\n return `Expected file to exist: ${RED}${path}${RESET}`;\n}\n\nexport function formatFileUnexpected(path: string): string {\n return `Expected file NOT to exist: ${RED}${path}${RESET}`;\n}\n\nexport function formatFileContentMismatch(path: string, expected: string, actual: string): string {\n const lines: string[] = [];\n lines.push(`File \"${path}\" does not contain expected content`);\n lines.push('');\n lines.push(`${GREEN}Expected to contain:${RESET}`);\n lines.push(` ${GREEN}${expected}${RESET}`);\n lines.push('');\n lines.push(`${RED}Actual content (first 20 lines):${RESET}`);\n for (const line of actual.split('\\n').slice(0, 20)) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n return lines.join('\\n');\n}\n\n// ── Service logs section ──\n\nexport function formatServiceLogs(services: { name: string; logs: string }[]): string {\n const lines: string[] = [];\n\n for (const { name, logs } of services) {\n if (!logs.trim()) {\n continue;\n }\n\n lines.push('');\n lines.push(`${DIM}${name} logs (last 10 lines):${RESET}`);\n\n const logLines = logs.trim().split('\\n').slice(-10);\n for (const line of logLines) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Helpers ──\n\nfunction rowLabel(n: number): string {\n return n === 1 ? '1 row' : `${n} rows`;\n}\n\nfunction formatJson(value: unknown, color: string): string {\n return JSON.stringify(value, null, 2)\n .split('\\n')\n .map((line) => `${color}${line}${RESET}`)\n .join('\\n');\n}\n\nfunction formatRow(row: unknown[]): string {\n return row.map((v) => String(v ?? 'null')).join(' | ');\n}\n\n// ── Test utilities ──\n\n/** Strip ANSI escape codes from a string. */\nexport function stripAnsi(str: string): string {\n // eslint-disable-next-line no-control-regex\n return str.replace(/\\x1b\\[[0-9;]*m/g, '');\n}\n\n/** Strip ANSI codes and replace volatile tokens (ports, durations) with placeholders. */\nexport function normalizeOutput(str: string): string {\n return stripAnsi(str)\n .replace(/localhost:\\d+/g, 'localhost:PORT')\n .replace(/\\d+ms/g, 'Xms')\n .replace(/\\d+\\.\\d+s/g, 'X.Xs')\n .trim();\n}\n","import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { diffDirectories, type DirectoryDiff, walkDirectory } from './directory.js';\nimport { formatDirectoryDiff } from './reporter.js';\n\nexport interface DirectorySnapshotOptions {\n /** Extra path segments to ignore (in addition to default: .git, node_modules, etc.). */\n ignore?: string[];\n /**\n * Force update mode regardless of vitest flags / env vars.\n * `true` writes the fixture, `false` always asserts. Defaults to auto-detect.\n */\n update?: boolean;\n}\n\n/**\n * Detect whether the user wants to update snapshots — `true` for any of:\n * - vitest run with `-u` / `--update`\n * - JTERRAZZ_TEST_UPDATE=1\n * - UPDATE_SNAPSHOTS=1\n */\nfunction shouldUpdateSnapshots(): boolean {\n if (process.env.JTERRAZZ_TEST_UPDATE === '1') {\n return true;\n }\n if (process.env.UPDATE_SNAPSHOTS === '1') {\n return true;\n }\n if (process.argv.includes('-u') || process.argv.includes('--update')) {\n return true;\n }\n return false;\n}\n\n/**\n * Handle to a directory produced by a specification run.\n * Supports snapshot-based assertions via {@link toMatchFixture} and file listing via {@link files}.\n */\nexport class DirectoryAccessor {\n private absPath: string;\n private testDir: string;\n\n constructor(absPath: string, testDir: string) {\n this.absPath = absPath;\n this.testDir = testDir;\n }\n\n /**\n * Compare the directory tree against `expected/{name}/` (relative to the test file).\n * On mismatch, throws with a structured diff. With update mode enabled, the\n * fixture is overwritten with the current contents instead.\n */\n async toMatchFixture(name: string, options: DirectorySnapshotOptions = {}): Promise<void> {\n const fixtureDir = resolve(this.testDir, 'expected', name);\n const update = options.update ?? shouldUpdateSnapshots();\n\n if (update) {\n rmSync(fixtureDir, { force: true, recursive: true });\n mkdirSync(fixtureDir, { recursive: true });\n cpSync(this.absPath, fixtureDir, { recursive: true });\n return;\n }\n\n if (!existsSync(fixtureDir)) {\n throw new Error(\n `Directory fixture \"${name}\" does not exist at ${fixtureDir}.\\n` +\n `Run with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`,\n );\n }\n\n const diff: DirectoryDiff = await diffDirectories(fixtureDir, this.absPath, {\n ignore: options.ignore,\n });\n\n if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {\n return;\n }\n\n throw new Error(\n formatDirectoryDiff(\n name,\n diff,\n 'Run with JTERRAZZ_TEST_UPDATE=1 to update the fixture.',\n ),\n );\n }\n\n /**\n * List all files in the directory (recursive, sorted, ignoring defaults).\n * Useful for ad-hoc assertions when you don't want a full snapshot.\n */\n async files(options: { ignore?: string[] } = {}): Promise<string[]> {\n return walkDirectory(this.absPath, options);\n }\n}\n","/**\n * Extract text blocks from output that contain a pattern.\n * Splits by blank lines (how linter/compiler output is structured),\n * returns only blocks matching the pattern.\n *\n * @example\n * expect(grep(result.stdout, \"unused-var.ts\")).toContain(\"no-unused-vars\")\n * expect(grep(result.stdout, \"valid/sorted.ts\")).not.toContain(\"sort-imports\")\n */\nexport function grep(output: string, pattern: string): string {\n // eslint-disable-next-line no-control-regex\n const clean = output.replace(/\\x1b\\[[0-9;]*m/g, '');\n const blocks = clean.split(/\\n\\s*\\n/);\n return blocks.filter((block) => block.includes(pattern)).join('\\n\\n');\n}\n","import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { formatResponseDiff } from './reporter.js';\n\n/** Accessor for an HTTP response body with file-based assertion support. */\nexport class ResponseAccessor {\n readonly body: unknown;\n private testDir: string;\n\n constructor(body: unknown, testDir: string) {\n this.body = body;\n this.testDir = testDir;\n }\n\n /**\n * Assert that the response body matches the JSON in `responses/{file}`.\n *\n * @example\n * result.response.toMatchFile(\"expected-items.json\");\n */\n toMatchFile(file: string): void {\n const expected = JSON.parse(readFileSync(resolve(this.testDir, 'responses', file), 'utf8'));\n if (JSON.stringify(this.body) !== JSON.stringify(expected)) {\n throw new Error(formatResponseDiff(file, expected, this.body));\n }\n }\n}\n","import type { DatabasePort } from '../../adapters/ports/database.port.js';\nimport { formatTableDiff } from './reporter.js';\n\n/** Assertion helper for verifying database table contents after a specification run. */\nexport class TableAssertion {\n private tableName: string;\n private db: DatabasePort;\n\n constructor(tableName: string, db: DatabasePort) {\n this.tableName = tableName;\n this.db = db;\n }\n\n /**\n * Assert that the table contains exactly the expected rows for the given columns.\n *\n * @example\n * await result.table(\"users\").toMatch({\n * columns: [\"name\", \"email\"],\n * rows: [[\"Alice\", \"alice@example.com\"]],\n * });\n */\n async toMatch(expected: { columns: string[]; rows: unknown[][] }): Promise<void> {\n const actual = await this.db.query(this.tableName, expected.columns);\n if (JSON.stringify(actual) !== JSON.stringify(expected.rows)) {\n throw new Error(\n formatTableDiff(this.tableName, expected.columns, expected.rows, actual),\n );\n }\n }\n\n /** Assert that the table has zero rows. */\n async toBeEmpty(): Promise<void> {\n const actual = await this.db.query(this.tableName, ['*']);\n if (actual.length !== 0) {\n throw new Error(\n `Expected table \"${this.tableName}\" to be empty, but it has ${actual.length} rows`,\n );\n }\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../../adapters/ports/database.port.js';\nimport type { CommandResult } from '../cli/command.port.js';\nimport type { ServerResponse } from '../http/server.port.js';\nimport type { SpecificationConfig } from '../specification-builder.js';\nimport { DirectoryAccessor } from './directory-accessor.js';\nimport { grep as grepUtil } from './grep.js';\nimport { ResponseAccessor } from './response-accessor.js';\nimport { TableAssertion } from './table-assertion.js';\n\n/** Read-only handle to a single file produced by a CLI action. */\nexport interface FileAccessor {\n /** The UTF-8 text content. Throws if the file does not exist. */\n readonly content: string;\n readonly exists: boolean;\n}\n\n/**\n * The outcome of a single specification run.\n * Provides accessors for CLI output, HTTP responses, files, directories, and database tables.\n */\nexport class SpecificationResult {\n private commandResult?: CommandResult;\n private config: SpecificationConfig;\n private requestInfo?: { body?: unknown; method: string; path: string };\n private responseData?: ServerResponse;\n private testDir: string;\n private workDir?: string;\n\n constructor(options: {\n commandResult?: CommandResult;\n config: SpecificationConfig;\n requestInfo?: { body?: unknown; method: string; path: string };\n response?: ServerResponse;\n testDir: string;\n workDir?: string;\n }) {\n this.responseData = options.response;\n this.commandResult = options.commandResult;\n this.config = options.config;\n this.testDir = options.testDir;\n this.requestInfo = options.requestInfo;\n this.workDir = options.workDir;\n }\n\n // ── Raw value accessors ──\n\n /** The process exit code. Only available after a CLI action. */\n get exitCode(): number {\n if (!this.commandResult) {\n throw new Error('.exitCode requires a CLI action (.exec())');\n }\n return this.commandResult.exitCode;\n }\n\n /** The HTTP response status code. Only available after an HTTP action. */\n get status(): number {\n if (!this.responseData) {\n throw new Error('.status requires an HTTP action (.get(), .post(), etc.)');\n }\n return this.responseData.status;\n }\n\n /** The captured standard output. Only available after a CLI action. */\n get stdout(): string {\n if (!this.commandResult) {\n throw new Error('.stdout requires a CLI action (.exec())');\n }\n return this.commandResult.stdout;\n }\n\n /** The captured standard error. Only available after a CLI action. */\n get stderr(): string {\n if (!this.commandResult) {\n throw new Error('.stderr requires a CLI action (.exec())');\n }\n return this.commandResult.stderr;\n }\n\n /**\n * Extract text blocks from stdout that contain a pattern.\n * Useful for parsing structured CLI output (linters, compilers).\n *\n * @example\n * expect(result.grep('error.ts')).toContain('no-unused-vars');\n */\n grep(pattern: string): string {\n return grepUtil(this.stdout, pattern);\n }\n\n // ── Structured accessors ──\n\n /** Access the HTTP response body for assertions. Only available after an HTTP action. */\n get response(): ResponseAccessor {\n if (!this.responseData) {\n throw new Error('.response requires an HTTP action (.get(), .post(), etc.)');\n }\n return new ResponseAccessor(this.responseData.body, this.testDir);\n }\n\n /** Access a directory (relative to the working directory) for snapshot assertions. */\n directory(path: string = '.'): DirectoryAccessor {\n const baseDir = this.workDir ?? this.testDir;\n return new DirectoryAccessor(resolve(baseDir, path), this.testDir);\n }\n\n /** Access a single file (relative to the working directory) for content assertions. */\n file(path: string): FileAccessor {\n const baseDir = this.workDir ?? this.testDir;\n const resolvedPath = resolve(baseDir, path);\n const exists = existsSync(resolvedPath);\n return {\n get content(): string {\n if (!exists) {\n throw new Error(`File not found: ${path}`);\n }\n return readFileSync(resolvedPath, 'utf8');\n },\n exists,\n };\n }\n\n /** Access a database table for row-level assertions. */\n table(tableName: string, options?: { service?: string }): TableAssertion {\n const db = this.resolveDatabase(options?.service);\n if (!db) {\n throw new Error(\n options?.service\n ? `table(\"${tableName}\") requires database \"${options.service}\" but it was not found`\n : `table(\"${tableName}\") requires a database adapter`,\n );\n }\n return new TableAssertion(tableName, db);\n }\n\n // ── Private ──\n\n private resolveDatabase(serviceName?: string): DatabasePort | undefined {\n if (serviceName && this.config.databases) {\n return this.config.databases.get(serviceName);\n }\n return this.config.database;\n }\n}\n","import { cpSync, existsSync, mkdtempSync, readFileSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../adapters/ports/database.port.js';\nimport type { CommandEnv, CommandPort, CommandResult, SpawnOptions } from './cli/command.port.js';\nimport type {\n InterceptEntry,\n InterceptResponse,\n InterceptTrigger,\n} from './common/intercept/types.js';\nimport { SpecificationResult } from './common/result.js';\nimport type { ServerPort } from './http/server.port.js';\n\n// ── Types ──\n\n/** A named job that can be triggered via .job(). */\nexport interface JobHandle {\n name: string;\n execute: () => Promise<void>;\n}\n\n/** Adapter configuration passed to the specification runner at setup time. */\nexport interface SpecificationConfig {\n command?: CommandPort;\n database?: DatabasePort;\n databases?: Map<string, DatabasePort>;\n fixturesRoot?: string;\n jobs?: JobHandle[];\n server?: ServerPort;\n}\n\n/** A SQL seed file to execute before the action, optionally targeting a named database service. */\nexport interface SeedEntry {\n file: string;\n service?: string;\n}\n\n/** A fixture file or directory to copy into the working directory before execution. */\nexport interface FixtureEntry {\n file: string;\n}\n\n/** An MSW mock definition file to register before the action. */\nexport interface MockEntry {\n file: string;\n}\n\n/** An HTTP request to perform against the server adapter. */\nexport interface RequestEntry {\n bodyFile?: string;\n headers?: Record<string, string>;\n method: string;\n path: string;\n}\n\n// ── Builder (before .run()) ──\n\n/**\n * Fluent builder for declaring a single test specification.\n *\n * Chain setup methods ({@link seed}, {@link fixture}, {@link env}), an action\n * ({@link get}, {@link post}, {@link exec}), then call {@link run} to execute\n * and receive a {@link SpecificationResult} for assertions.\n */\nexport class SpecificationBuilder {\n private commandArgs: null | string | string[] = null;\n private commandEnv: CommandEnv = {};\n private config: SpecificationConfig;\n private fixtures: FixtureEntry[] = [];\n private intercepts: InterceptEntry[] = [];\n private jobName: null | string = null;\n private label: string;\n private mocks: MockEntry[] = [];\n private projectName: null | string = null;\n private request: null | RequestEntry = null;\n private requestHeaders: Record<string, string> = {};\n private seeds: SeedEntry[] = [];\n private spawnConfig: null | { args: string; options: SpawnOptions } = null;\n private testDir: string;\n\n constructor(config: SpecificationConfig, testDir: string, label: string) {\n this.config = config;\n this.testDir = testDir;\n this.label = label;\n }\n\n // ── Setup ──\n\n /**\n * Queue a SQL seed file to run before the action.\n *\n * @example\n * spec(\"creates user\").seed(\"users.sql\").exec(\"list-users\").run();\n */\n seed(file: string, options?: { service?: string }): this {\n this.seeds.push({ file, service: options?.service });\n return this;\n }\n\n /** Copy a file or directory from `fixtures/` into the working directory before execution. */\n fixture(file: string): this {\n this.fixtures.push({ file });\n return this;\n }\n\n /** Copy an entire project fixture from `fixturesRoot` into the working directory. */\n project(name: string): this {\n this.projectName = name;\n return this;\n }\n\n /** Register an MSW mock definition file from `mock/` to intercept HTTP calls. */\n mock(file: string): this {\n this.mocks.push({ file });\n return this;\n }\n\n /**\n * Set environment variables for the CLI process. Merged on top of process.env.\n * Use `null` to unset a variable. Multiple calls merge.\n *\n * The token `$WORKDIR` (in any value) is replaced with the actual working\n * directory at run-time — useful for tests that need a fully isolated `HOME`.\n *\n * @example\n * spec(\"...\").env({ HOME: \"$WORKDIR\", TZ: \"UTC\" }).exec(\"status\").run();\n */\n env(env: CommandEnv): this {\n this.commandEnv = { ...this.commandEnv, ...env };\n return this;\n }\n\n /**\n * Set HTTP headers for the request. Multiple calls merge.\n *\n * @example\n * spec(\"french\").headers({ 'Accept-Language': 'fr' }).get(\"/articles\").run();\n */\n headers(headers: Record<string, string>): this {\n this.requestHeaders = { ...this.requestHeaders, ...headers };\n return this;\n }\n\n /**\n * Intercept an outgoing HTTP request and return a controlled response.\n * Uses MSW under the hood. Intercepts are queued — multiple calls with the\n * same trigger fire sequentially (first match consumed first).\n *\n * @param trigger - What to match (use openai.agent(), http.get(), etc.)\n * @param response - What to return: an InterceptResponse object, or a filename\n * from `intercepts/` (JSON loaded and auto-wrapped by the trigger).\n *\n * @example\n * // With inline response\n * .intercept(openai.agent({...}), openai.reply({ categories: ['TECH'] }))\n *\n * // With JSON fixture file (loaded from intercepts/ directory)\n * .intercept(openai.agent({...}), 'ingest-tech.json')\n * .intercept(http.get(url), 'world-news-tech.json')\n */\n intercept(trigger: InterceptTrigger, response: InterceptResponse | string): this {\n if (typeof response === 'string') {\n // File path format: 'adapter/filename.json'\n const slashIndex = response.indexOf('/');\n if (slashIndex === -1) {\n throw new Error(\n `.intercept(): file path must be 'adapter/filename.json' (e.g. 'openai/ingest-tech.json'), got '${response}'`,\n );\n }\n\n const adapterName = response.slice(0, slashIndex);\n if (adapterName !== trigger.adapter) {\n throw new Error(\n `.intercept(): adapter mismatch - trigger uses '${trigger.adapter}' but file path starts with '${adapterName}/'`,\n );\n }\n\n const filePath = resolve(this.testDir, 'intercepts', response);\n const data = JSON.parse(readFileSync(filePath, 'utf8'));\n const resolved = trigger.wrap(data);\n this.intercepts.push({ trigger, response: resolved });\n } else {\n this.intercepts.push({ trigger, response });\n }\n return this;\n }\n\n // ── HTTP actions ──\n\n /**\n * Send a GET request to the server adapter.\n *\n * @example\n * spec(\"list items\").get(\"/api/items\").run();\n */\n get(path: string): this {\n this.request = { method: 'GET', path };\n return this;\n }\n\n /**\n * Send a POST request to the server adapter.\n *\n * @param bodyFile - Optional JSON file from `requests/` to use as the request body.\n * @example\n * spec(\"create item\").post(\"/api/items\", \"new-item.json\").run();\n */\n post(path: string, bodyFile?: string): this {\n this.request = { bodyFile, method: 'POST', path };\n return this;\n }\n\n /** Send a PUT request to the server adapter. */\n put(path: string, bodyFile?: string): this {\n this.request = { bodyFile, method: 'PUT', path };\n return this;\n }\n\n /** Send a DELETE request to the server adapter. */\n delete(path: string): this {\n this.request = { method: 'DELETE', path };\n return this;\n }\n\n // ── CLI actions ──\n\n /**\n * Execute a CLI command (or a sequence of commands) in an isolated working directory.\n * When an array is passed, commands run sequentially and stop on the first non-zero exit code.\n *\n * @example\n * spec(\"init project\").exec(\"init --name demo\").run();\n * spec(\"multi-step\").exec([\"init\", \"build\"]).run();\n */\n exec(args: string | string[]): this {\n this.commandArgs = args;\n return this;\n }\n\n /** Spawn a long-running CLI process with custom spawn options (e.g. timeout, signal). */\n spawn(args: string, options: SpawnOptions): this {\n this.spawnConfig = { args, options };\n return this;\n }\n\n // ── Job actions ──\n\n /**\n * Execute a named job registered via the app() factory.\n *\n * @param name - The job name to trigger (must match a registered JobHandle.name).\n *\n * @example\n * spec('pipeline').intercept(openai.chat(), openai.response({...})).job('report-refresh').run();\n */\n job(name: string): this {\n this.jobName = name;\n return this;\n }\n\n // ── Run ──\n\n /**\n * Execute the specification: run seeds, copy fixtures, then perform the\n * configured action (HTTP or CLI).\n *\n * @returns The result object used for assertions.\n * @example\n * const result = await spec(\"test\").exec(\"status\").run();\n * expect(result.exitCode).toBe(0);\n */\n async run(): Promise<SpecificationResult> {\n const hasHttpAction = this.request !== null;\n const hasCliAction = this.commandArgs !== null || this.spawnConfig !== null;\n const hasJobAction = this.jobName !== null;\n\n const actionCount = [hasHttpAction, hasCliAction, hasJobAction].filter(Boolean).length;\n\n if (actionCount === 0) {\n throw new Error(\n `Specification \"${this.label}\": no action defined. Call .get(), .post(), .exec(), .job(), etc. before .run()`,\n );\n }\n\n if (actionCount > 1) {\n throw new Error(\n `Specification \"${this.label}\": cannot mix action types (.get/.post, .exec/.spawn, .job)`,\n );\n }\n\n let workDir: null | string = null;\n if (hasCliAction) {\n workDir = this.prepareWorkDir();\n }\n\n // Reset all databases\n if (this.config.databases) {\n for (const db of this.config.databases.values()) {\n await db.reset();\n }\n } else if (this.config.database) {\n await this.config.database.reset();\n }\n\n // Execute seeds\n for (const entry of this.seeds) {\n let db: DatabasePort | undefined;\n if (entry.service && this.config.databases) {\n db = this.config.databases.get(entry.service);\n if (!db) {\n throw new Error(\n `seed() targets database \"${entry.service}\" but it was not found. Available: ${[...this.config.databases.keys()].join(', ')}`,\n );\n }\n } else {\n db = this.config.database;\n }\n\n if (!db) {\n throw new Error('seed() requires a database adapter');\n }\n\n const sql = readFileSync(resolve(this.testDir, 'seeds', entry.file), 'utf8');\n await db.seed(sql);\n }\n\n // Copy fixture files into working directory\n if (this.fixtures.length > 0 && workDir) {\n for (const entry of this.fixtures) {\n const src = resolve(this.testDir, 'fixtures', entry.file);\n const dest = resolve(workDir, entry.file);\n cpSync(src, dest, { recursive: true });\n }\n }\n\n // Register HTTP intercepts via MSW\n let cleanupIntercepts: (() => void) | null = null;\n if (this.intercepts.length > 0) {\n const { registerIntercepts } = await import('./common/intercept/intercept.js');\n cleanupIntercepts = await registerIntercepts(this.intercepts);\n }\n\n // Execute action\n try {\n if (hasHttpAction) {\n return await this.runHttpAction();\n }\n if (hasJobAction) {\n return await this.runJobAction();\n }\n return await this.runCliAction(workDir!);\n } finally {\n if (cleanupIntercepts) {\n cleanupIntercepts();\n }\n }\n }\n\n // ── Private ──\n\n private resolveEnv(workDir: string): CommandEnv | undefined {\n const keys = Object.keys(this.commandEnv);\n if (keys.length === 0) {\n return undefined;\n }\n\n const resolved: CommandEnv = {};\n for (const key of keys) {\n const value = this.commandEnv[key];\n resolved[key] =\n typeof value === 'string' ? value.replace(/\\$WORKDIR/g, workDir) : value;\n }\n return resolved;\n }\n\n private prepareWorkDir(): string {\n // Every CLI spec runs in a fresh, empty temp directory unless a project\n // Fixture is explicitly copied in via .project() (or files via .fixture()).\n // This guarantees isolation — the runner never writes into fixturesRoot.\n const tempDir = mkdtempSync(resolve(tmpdir(), 'spec-cli-'));\n\n if (this.projectName && this.config.fixturesRoot) {\n const projectDir = resolve(this.config.fixturesRoot, this.projectName);\n if (!existsSync(projectDir)) {\n throw new Error(\n `project(\"${this.projectName}\"): fixture project not found at ${projectDir}`,\n );\n }\n cpSync(projectDir, tempDir, { recursive: true });\n }\n\n return tempDir;\n }\n\n private async runHttpAction(): Promise<SpecificationResult> {\n if (!this.config.server) {\n throw new Error('HTTP actions require a server adapter (use integration() or e2e())');\n }\n\n let body: unknown;\n if (this.request!.bodyFile) {\n body = JSON.parse(\n readFileSync(resolve(this.testDir, 'requests', this.request!.bodyFile), 'utf8'),\n );\n }\n\n const headers =\n Object.keys(this.requestHeaders).length > 0 ? this.requestHeaders : undefined;\n const response = await this.config.server.request(\n this.request!.method,\n this.request!.path,\n body,\n headers,\n );\n\n return new SpecificationResult({\n config: this.config,\n requestInfo: { body, method: this.request!.method, path: this.request!.path },\n response,\n testDir: this.testDir,\n });\n }\n\n private async runJobAction(): Promise<SpecificationResult> {\n if (!this.config.jobs?.length) {\n throw new Error(\n 'Job actions require jobs registered via app(() => ({ server, jobs }))',\n );\n }\n\n const job = this.config.jobs.find((j) => j.name === this.jobName);\n if (!job) {\n const available = this.config.jobs.map((j) => j.name).join(', ');\n throw new Error(`job(\"${this.jobName}\"): not found. Available: ${available}`);\n }\n\n await job.execute();\n\n return new SpecificationResult({\n config: this.config,\n testDir: this.testDir,\n });\n }\n\n private async runCliAction(workDir: string): Promise<SpecificationResult> {\n if (!this.config.command) {\n throw new Error('CLI actions require a command adapter (use cli())');\n }\n\n const env = this.resolveEnv(workDir);\n let commandResult: CommandResult;\n\n if (this.spawnConfig) {\n commandResult = await this.config.command.spawn(\n this.spawnConfig.args,\n workDir,\n this.spawnConfig.options,\n env,\n );\n } else if (Array.isArray(this.commandArgs)) {\n commandResult = { exitCode: 0, stderr: '', stdout: '' };\n for (const args of this.commandArgs) {\n commandResult = await this.config.command.exec(args, workDir, env);\n if (commandResult.exitCode !== 0) {\n break;\n }\n }\n } else {\n commandResult = await this.config.command.exec(this.commandArgs!, workDir, env);\n }\n\n return new SpecificationResult({\n commandResult,\n config: this.config,\n testDir: this.testDir,\n workDir,\n });\n }\n}\n\n// ── Caller detection ──\n\nfunction getCallerDir(): string {\n const stack = new Error('caller detection').stack;\n if (!stack) {\n throw new Error('Cannot detect caller directory: no stack trace');\n }\n\n const lines = stack.split('\\n');\n for (const line of lines) {\n const match = line.match(/at\\s+(?:.*?\\()?(?:file:\\/\\/)?([^:)]+):\\d+:\\d+/);\n if (!match) {\n continue;\n }\n\n const filePath = match[1];\n\n if (filePath.includes('node_modules')) {\n continue;\n }\n if (filePath.includes('/src/builder/') || filePath.includes('/src/spec/')) {\n continue;\n }\n\n return resolve(filePath, '..');\n }\n\n throw new Error('Cannot detect caller directory from stack trace');\n}\n\n// ── Factory functions ──\n\n/** Factory function returned by `cli()`, `integration()`, or `e2e()` that starts a new spec. */\nexport type SpecificationRunner = (label: string) => SpecificationBuilder;\n\n/**\n * Create a {@link SpecificationRunner} bound to the given adapter configuration.\n * The test file directory is auto-detected from the call stack.\n */\nexport function createSpecificationRunner(config: SpecificationConfig): SpecificationRunner {\n return (label: string) => {\n const testDir = getCallerDir();\n return new SpecificationBuilder(config, testDir, label);\n };\n}\n","import { execSync } from 'node:child_process';\n\nimport type { DockerContainerPort, DockerInspectResult } from './docker-port.js';\n\n/**\n * Implements {@link DockerContainerPort} by shelling out to the Docker CLI.\n * Each instance is bound to a single container ID.\n */\nexport class DockerAdapter implements DockerContainerPort {\n private containerId: string;\n\n constructor(containerId: string) {\n this.containerId = containerId;\n }\n\n async exec(cmd: string[]): Promise<string> {\n const result = execSync(\n `docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(' ')}`,\n { encoding: 'utf8', timeout: 10_000 },\n );\n return result.trim();\n }\n\n async file(path: string): Promise<{ exists: boolean; content: string }> {\n try {\n const content = await this.exec(['cat', path]);\n return { exists: true, content };\n } catch {\n return { exists: false, content: '' };\n }\n }\n\n async isRunning(): Promise<boolean> {\n try {\n const result = execSync(\n `docker inspect --format='{{.State.Running}}' ${this.containerId}`,\n {\n encoding: 'utf8',\n timeout: 5000,\n },\n );\n return result.trim() === 'true';\n } catch {\n return false;\n }\n }\n\n async logs(tail?: number): Promise<string> {\n const tailFlag = tail ? `--tail ${tail}` : '';\n const result = execSync(`docker logs ${tailFlag} ${this.containerId}`, {\n encoding: 'utf8',\n timeout: 10_000,\n });\n return result;\n }\n\n async inspect(): Promise<DockerInspectResult> {\n const raw = execSync(`docker inspect ${this.containerId}`, {\n encoding: 'utf8',\n timeout: 5000,\n });\n const data = JSON.parse(raw)[0];\n return {\n id: data.Id,\n name: data.Name,\n state: {\n running: data.State.Running,\n exitCode: data.State.ExitCode,\n status: data.State.Status,\n },\n config: {\n image: data.Config.Image,\n env: data.Config.Env || [],\n },\n hostConfig: {\n memory: data.HostConfig.Memory || 0,\n cpuQuota: data.HostConfig.CpuQuota || 0,\n networkMode: data.HostConfig.NetworkMode || '',\n mounts: (data.Mounts || []).map((m: any) => ({\n source: m.Source,\n destination: m.Destination,\n type: m.Type,\n })),\n },\n networkSettings: {\n networks: Object.fromEntries(\n Object.entries(data.NetworkSettings.Networks || {}).map(\n ([name, net]: [string, any]) => [\n name,\n { gateway: net.Gateway, ipAddress: net.IPAddress },\n ],\n ),\n ),\n },\n };\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await this.exec(['test', '-e', path]);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Create a {@link DockerContainerPort} bound to an existing container by ID. */\nexport function dockerContainer(containerId: string): DockerContainerPort {\n return new DockerAdapter(containerId);\n}\n","import type { DockerContainerPort } from './docker-port.js';\n\n/**\n * Fluent assertion builder for Docker containers.\n * Chain async assertions like `toBeRunning()`, `toHaveFile()`, `toHaveMount()`.\n */\nexport class DockerAssertion {\n private container: DockerContainerPort;\n\n constructor(container: DockerContainerPort) {\n this.container = container;\n }\n\n /** Assert the container is running */\n async toBeRunning(): Promise<this> {\n const running = await this.container.isRunning();\n if (!running) {\n throw new Error('Expected container to be running');\n }\n return this;\n }\n\n /** Assert the container is NOT running / doesn't exist */\n async toNotExist(): Promise<this> {\n const running = await this.container.isRunning();\n if (running) {\n throw new Error('Expected container to not exist or not be running');\n }\n return this;\n }\n\n /** Assert a file exists inside the container */\n async toHaveFile(path: string, opts?: { containing?: string }): Promise<this> {\n const file = await this.container.file(path);\n if (!file.exists) {\n throw new Error(`Expected file ${path} to exist in container`);\n }\n if (opts?.containing && !file.content.includes(opts.containing)) {\n throw new Error(\n `Expected file ${path} to contain \"${opts.containing}\", got: ${file.content.slice(0, 200)}`,\n );\n }\n return this;\n }\n\n /** Assert a file does NOT exist */\n async toNotHaveFile(path: string): Promise<this> {\n const exists = await this.container.exists(path);\n if (exists) {\n throw new Error(`Expected ${path} to not exist in container`);\n }\n return this;\n }\n\n /** Assert a directory exists */\n async toHaveDirectory(path: string): Promise<this> {\n const exists = await this.container.exists(path);\n if (!exists) {\n throw new Error(`Expected directory ${path} to exist in container`);\n }\n return this;\n }\n\n /** Assert a mount exists */\n async toHaveMount(destination: string): Promise<this> {\n const info = await this.container.inspect();\n const mount = info.hostConfig.mounts.find((m) => m.destination === destination);\n if (!mount) {\n const available = info.hostConfig.mounts.map((m) => m.destination).join(', ');\n throw new Error(`Expected mount at ${destination}, found: [${available}]`);\n }\n return this;\n }\n\n /** Assert network mode */\n async toHaveNetwork(mode: string): Promise<this> {\n const info = await this.container.inspect();\n if (!info.hostConfig.networkMode.includes(mode)) {\n throw new Error(\n `Expected network mode \"${mode}\", got \"${info.hostConfig.networkMode}\"`,\n );\n }\n return this;\n }\n\n /** Assert memory limit */\n async toHaveMemoryLimit(bytes: number): Promise<this> {\n const info = await this.container.inspect();\n if (info.hostConfig.memory !== bytes) {\n throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);\n }\n return this;\n }\n\n /** Assert CPU quota */\n async toHaveCpuQuota(quota: number): Promise<this> {\n const info = await this.container.inspect();\n if (info.hostConfig.cpuQuota !== quota) {\n throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);\n }\n return this;\n }\n\n /** Execute a command and return output for custom assertions */\n async exec(cmd: string[]): Promise<string> {\n return this.container.exec(cmd);\n }\n\n /** Read a file for custom assertions */\n async readFile(path: string): Promise<string> {\n const file = await this.container.file(path);\n if (!file.exists) {\n throw new Error(`File ${path} does not exist`);\n }\n return file.content;\n }\n\n /** Get logs for custom assertions */\n async getLogs(tail?: number): Promise<string> {\n return this.container.logs(tail);\n }\n}\n","import { execSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\nimport type { ContainerPort } from '../ports/container.port.js';\n\n/**\n * Container adapter using docker compose — runs full compose stack.\n * Used by e2e() to start all services including the app.\n */\nexport class ComposeAdapter implements ContainerPort {\n private composeFile: string;\n private serviceName: string;\n private started = false;\n\n constructor(composeFile: string, serviceName: string) {\n this.composeFile = composeFile;\n this.serviceName = serviceName;\n }\n\n private exec(command: string): string {\n return execSync(command, {\n cwd: dirname(this.composeFile),\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'inherit'],\n }).trim();\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n this.exec(`docker compose -f ${this.composeFile} up -d --wait ${this.serviceName}`);\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n this.exec(`docker compose -f ${this.composeFile} rm -fsv ${this.serviceName}`);\n this.started = false;\n }\n\n getMappedPort(containerPort: number): number {\n const output = this.exec(\n `docker compose -f ${this.composeFile} port ${this.serviceName} ${containerPort}`,\n );\n // Output: 0.0.0.0:54321\n const port = output.split(':').pop();\n return Number(port);\n }\n\n getHost(): string {\n return 'localhost';\n }\n\n getConnectionString(): string {\n return `${this.getHost()}:${this.getMappedPort(0)}`;\n }\n\n async getLogs(): Promise<string> {\n try {\n return this.exec(\n `docker compose -f ${this.composeFile} logs ${this.serviceName} --tail=50`,\n );\n } catch {\n return '';\n }\n }\n}\n\n/**\n * Start the full compose stack and stop it all on cleanup.\n * Supports per-worker project names for parallel execution.\n */\nexport class ComposeStackAdapter {\n private composeFile: string;\n private projectName: null | string;\n private started = false;\n\n constructor(composeFile: string, projectName?: string) {\n this.composeFile = composeFile;\n this.projectName = projectName ?? null;\n }\n\n private get projectFlag(): string {\n return this.projectName ? `-p ${this.projectName}` : '';\n }\n\n private run(command: string): string {\n try {\n return execSync(command, {\n cwd: dirname(this.composeFile),\n encoding: 'utf8',\n timeout: 120_000,\n }).trim();\n } catch (error: any) {\n const stderr = error.stderr?.toString().trim() ?? error.message;\n throw new Error(`docker compose failed: ${stderr}`, { cause: error });\n }\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} up -d --wait`);\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} down -v`);\n this.started = false;\n }\n\n getMappedPort(serviceName: string, containerPort: number): number {\n const output = this.run(\n `docker compose ${this.projectFlag} -f ${this.composeFile} port ${serviceName} ${containerPort}`,\n );\n const port = output.split(':').pop();\n return Number(port);\n }\n\n getHost(): string {\n return 'localhost';\n }\n}\n","import type { ContainerPort } from '../ports/container.port.js';\n\n/**\n * Container adapter using testcontainers.\n * Wraps a GenericContainer for programmatic container lifecycle.\n */\nexport class TestcontainersAdapter implements ContainerPort {\n private image: string;\n private containerPort: number;\n private env: Record<string, string>;\n private reuse: boolean;\n private container: any = null;\n\n constructor(options: {\n image: string;\n port: number;\n env?: Record<string, string>;\n reuse?: boolean;\n }) {\n this.image = options.image;\n this.containerPort = options.port;\n this.env = options.env ?? {};\n this.reuse = options.reuse ?? false;\n }\n\n async start(): Promise<void> {\n const { GenericContainer, Wait } = await import('testcontainers');\n\n let builder = new GenericContainer(this.image).withExposedPorts(this.containerPort);\n\n for (const [key, value] of Object.entries(this.env)) {\n builder = builder.withEnvironment({ [key]: value });\n }\n\n if (this.image.startsWith('postgres')) {\n builder = builder.withWaitStrategy(\n Wait.forLogMessage(/database system is ready to accept connections/, 2),\n );\n }\n\n if (this.reuse) {\n builder = builder.withReuse();\n }\n\n this.container = await builder.start();\n }\n\n async stop(): Promise<void> {\n if (this.container && !this.reuse) {\n await this.container.stop();\n this.container = null;\n }\n }\n\n getMappedPort(containerPort: number): number {\n if (!this.container) {\n throw new Error('Container not started');\n }\n return this.container.getMappedPort(containerPort);\n }\n\n getHost(): string {\n if (!this.container) {\n throw new Error('Container not started');\n }\n return this.container.getHost();\n }\n\n getConnectionString(): string {\n return `${this.getHost()}:${this.getMappedPort(this.containerPort)}`;\n }\n\n async getLogs(): Promise<string> {\n if (!this.container) {\n return '';\n }\n\n const stream = await this.container.logs();\n return new Promise((resolve) => {\n let output = '';\n stream.on('data', (chunk: Buffer) => {\n output += chunk.toString();\n });\n stream.on('end', () => {\n resolve(output);\n });\n // Timeout after 1s if stream doesn't end\n setTimeout(() => {\n resolve(output);\n }, 1000);\n });\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\n/**\n * A parsed service from docker-compose.test.yaml.\n */\nexport interface ComposeService {\n name: string;\n image?: string;\n build?: string;\n ports: { container: number; host?: number }[];\n environment: Record<string, string>;\n volumes: string[];\n dependsOn: string[];\n}\n\n/**\n * Result of parsing a compose file.\n */\nexport interface ComposeConfig {\n services: ComposeService[];\n appService: ComposeService | null;\n infraServices: ComposeService[];\n}\n\n/**\n * Detect the service type from the image name.\n */\nexport function detectServiceType(\n image: string | undefined,\n): 'app' | 'postgres' | 'redis' | 'unknown' {\n if (!image) {\n return 'app';\n }\n\n const lower = image.toLowerCase();\n\n if (lower.startsWith('postgres')) {\n return 'postgres';\n }\n if (lower.startsWith('redis')) {\n return 'redis';\n }\n\n return 'unknown';\n}\n\n/**\n * Find the compose file in the project.\n * Looks for docker/compose.test.yaml or docker-compose.test.yaml.\n */\nexport function findComposeFile(projectRoot: string): null | string {\n const candidates = [\n resolve(projectRoot, 'docker/compose.test.yaml'),\n resolve(projectRoot, 'docker/compose.test.yml'),\n resolve(projectRoot, 'docker-compose.test.yaml'),\n resolve(projectRoot, 'docker-compose.test.yml'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Parse a docker-compose file and extract service definitions.\n */\nexport function parseComposeFile(filePath: string): ComposeConfig {\n const content = readFileSync(filePath, 'utf8');\n const doc = parseYaml(content);\n\n if (!doc?.services) {\n return { services: [], appService: null, infraServices: [] };\n }\n\n const services: ComposeService[] = Object.entries(doc.services).map(\n ([name, def]: [string, any]) => {\n const ports: { container: number; host?: number }[] = [];\n if (def.ports) {\n for (const port of def.ports) {\n const str = String(port);\n if (str.includes(':')) {\n const [host, container] = str.split(':');\n ports.push({ container: Number(container), host: Number(host) });\n } else {\n ports.push({ container: Number(str) });\n }\n }\n }\n\n const environment: Record<string, string> = {};\n if (def.environment) {\n if (Array.isArray(def.environment)) {\n for (const env of def.environment) {\n const [key, ...rest] = String(env).split('=');\n environment[key] = rest.join('=');\n }\n } else {\n Object.assign(environment, def.environment);\n }\n }\n\n const volumes: string[] = def.volumes ? def.volumes.map((v: string) => String(v)) : [];\n\n let dependsOn: string[] = [];\n if (def.depends_on) {\n dependsOn = Array.isArray(def.depends_on)\n ? def.depends_on\n : Object.keys(def.depends_on);\n }\n\n return {\n name,\n image: def.image,\n build: def.build,\n ports,\n environment,\n volumes,\n dependsOn,\n };\n },\n );\n\n const appService = services.find((s) => s.build !== undefined) ?? null;\n const infraServices = services.filter((s) => s.build === undefined);\n\n return { services, appService, infraServices };\n}\n","import { dirname } from 'node:path';\n\nimport type { DatabasePort } from '../adapters/ports/database.port.js';\nimport { postgres } from '../adapters/postgres.adapter.js';\nimport { redis } from '../adapters/redis.adapter.js';\nimport {\n type AppInfo,\n formatStartupReport,\n type ServiceReport,\n} from '../builder/common/reporter.js';\nimport { ComposeStackAdapter } from './adapters/compose.adapter.js';\nimport { TestcontainersAdapter } from './adapters/testcontainers.adapter.js';\nimport { detectServiceType, findComposeFile, parseComposeFile } from './compose-parser.js';\nimport type { ContainerPort } from './ports/container.port.js';\nimport type { ServiceHandle } from './ports/service.port.js';\n\ninterface RunningService {\n handle: ServiceHandle;\n container: ContainerPort | null;\n}\n\nexport interface OrchestratorOptions {\n services: ServiceHandle[];\n mode: 'e2e' | 'integration';\n root?: string;\n /** Compose project name — used for per-worker stack isolation. */\n projectName?: string;\n}\n\n/**\n * Orchestrator for test infrastructure.\n * Integration: starts services via testcontainers.\n * E2E: runs full docker compose up.\n */\nexport class Orchestrator {\n private services: ServiceHandle[];\n private mode: 'e2e' | 'integration';\n private root: string;\n private projectName: string | undefined;\n private running: RunningService[] = [];\n private composeStack: ComposeStackAdapter | null = null;\n private composeHandles: ServiceHandle[] = [];\n private started = false;\n\n constructor(options: OrchestratorOptions) {\n this.services = options.services;\n this.mode = options.mode;\n this.root = options.root ?? process.cwd();\n this.projectName = options.projectName;\n }\n\n /**\n * Start declared services via testcontainers (integration mode).\n * Phase 1: start all containers in parallel (the slow part).\n * Phase 2: wire connections, healthcheck, and init sequentially (fast).\n */\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n const composePath = findComposeFile(this.root);\n const composeDir = composePath ? dirname(composePath) : this.root;\n const composeConfig = composePath ? parseComposeFile(composePath) : null;\n\n // Separate services that need containers from embedded ones (e.g. SQLite)\n const containerServices: { container: TestcontainersAdapter; handle: ServiceHandle }[] = [];\n const embeddedServices: ServiceHandle[] = [];\n\n for (const handle of this.services) {\n if (handle.defaultPort === 0) {\n // Embedded service (no container needed)\n embeddedServices.push(handle);\n continue;\n }\n\n let image = handle.defaultImage;\n let env = { ...handle.environment };\n\n if (handle.composeName && composeConfig) {\n const composeService = composeConfig.services.find(\n (s) => s.name === handle.composeName,\n );\n if (composeService) {\n image = composeService.image ?? image;\n env = { ...env, ...composeService.environment };\n Object.assign(handle.environment, composeService.environment);\n }\n }\n\n const container = new TestcontainersAdapter({ image, port: handle.defaultPort, env });\n containerServices.push({ container, handle });\n }\n\n // Phase 1: start containers in parallel + initialize embedded services\n await Promise.all([\n ...containerServices.map(({ container }) => container.start()),\n ...embeddedServices.map(async (handle) => {\n await handle.initialize(composeDir);\n handle.started = true;\n this.running.push({ handle, container: null });\n }),\n ]);\n\n // Phase 2: wire connections, healthcheck, init (fast — containers already running)\n const reports: ServiceReport[] = [];\n\n for (const { container, handle } of containerServices) {\n const serviceStartTime = Date.now();\n\n try {\n const host = container.getHost();\n const port = container.getMappedPort(handle.defaultPort);\n handle.connectionString = handle.buildConnectionString(host, port);\n\n await handle.healthcheck();\n await handle.initialize(composeDir);\n handle.started = true;\n\n reports.push({\n name: handle.composeName ?? handle.type,\n type: handle.type,\n connectionString: handle.connectionString,\n durationMs: Date.now() - serviceStartTime,\n });\n this.running.push({ handle, container });\n } catch (error: any) {\n let logs = '';\n try {\n logs = await container.getLogs();\n } catch {\n /* Ignore log fetch errors */\n }\n try {\n await container.stop();\n } catch {\n /* Ignore stop errors */\n }\n\n reports.push({\n name: handle.composeName ?? handle.type,\n type: handle.type,\n durationMs: Date.now() - serviceStartTime,\n error: error.message,\n logs,\n });\n\n const output = formatStartupReport('integration', reports, { type: 'in-process' });\n console.error(output);\n throw error;\n }\n }\n\n this.started = true;\n\n const appInfo: AppInfo = { type: 'in-process' };\n const output = formatStartupReport('integration', reports, appInfo);\n console.log(output);\n }\n\n /**\n * Stop testcontainers (integration mode).\n */\n async stop(): Promise<void> {\n for (const { container } of this.running) {\n if (container) {\n await container.stop();\n }\n }\n this.running = [];\n this.started = false;\n }\n\n /**\n * Start full docker compose stack (e2e mode).\n * Auto-detects infra services and creates handles for them.\n */\n async startCompose(): Promise<void> {\n const composePath = findComposeFile(this.root);\n if (!composePath) {\n throw new Error(`E2E: no compose file found in ${this.root}`);\n }\n\n const startTime = Date.now();\n const composeDir = dirname(composePath);\n const composeConfig = parseComposeFile(composePath);\n\n this.composeStack = new ComposeStackAdapter(composePath, this.projectName);\n await this.composeStack.start();\n\n // Create handles for detected infra services\n for (const service of composeConfig.infraServices) {\n const type = detectServiceType(service.image);\n\n if (type === 'postgres') {\n const handle = postgres({ compose: service.name, env: service.environment });\n const port = this.composeStack.getMappedPort(service.name, 5432);\n handle.connectionString = handle.buildConnectionString('localhost', port);\n\n await handle.initialize(composeDir);\n handle.started = true;\n\n this.composeHandles.push(handle);\n } else if (type === 'redis') {\n const handle = redis({ compose: service.name });\n const port = this.composeStack.getMappedPort(service.name, 6379);\n handle.connectionString = handle.buildConnectionString('localhost', port);\n handle.started = true;\n\n this.composeHandles.push(handle);\n }\n }\n\n const durationMs = Date.now() - startTime;\n const reports: ServiceReport[] = this.composeHandles.map((h) => ({\n name: h.composeName ?? h.type,\n type: h.type,\n connectionString: h.connectionString,\n durationMs,\n }));\n\n const appUrl = this.getAppUrl();\n const appInfo: AppInfo = { type: 'http', url: appUrl ?? undefined };\n const output = formatStartupReport('e2e', reports, appInfo);\n console.log(output);\n }\n\n /**\n * Stop docker compose stack (e2e mode).\n */\n async stopCompose(): Promise<void> {\n if (this.composeStack) {\n await this.composeStack.stop();\n this.composeStack = null;\n }\n this.composeHandles = [];\n }\n\n /**\n * Get a database service by compose name, or the first one if no name given.\n */\n getDatabase(serviceName?: string): DatabasePort | null {\n for (const handle of [...this.services, ...this.composeHandles]) {\n if (serviceName && handle.composeName !== serviceName) {\n continue;\n }\n const adapter = handle.createDatabaseAdapter();\n if (adapter) {\n return adapter;\n }\n }\n return null;\n }\n\n /**\n * Get all database services keyed by compose name.\n */\n getDatabases(): Map<string, DatabasePort> {\n const map = new Map<string, DatabasePort>();\n for (const handle of [...this.services, ...this.composeHandles]) {\n const adapter = handle.createDatabaseAdapter();\n if (adapter && handle.composeName) {\n map.set(handle.composeName, adapter);\n }\n }\n return map;\n }\n\n /**\n * Get app URL from compose (e2e mode).\n */\n getAppUrl(): null | string {\n const composePath = findComposeFile(this.root);\n if (!composePath || !this.composeStack) {\n return null;\n }\n\n const config = parseComposeFile(composePath);\n const appService = config.appService;\n\n if (!appService || appService.ports.length === 0) {\n return null;\n }\n\n const port = this.composeStack.getMappedPort(\n appService.name,\n appService.ports[0].container,\n );\n return `http://localhost:${port}`;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\n/**\n * Resolve root — if relative, resolves from the caller's directory.\n */\nexport function resolveProjectRoot(root: string | undefined): string {\n if (!root) {\n return process.cwd();\n }\n\n if (isAbsolute(root)) {\n return root;\n }\n\n const stack = new Error('resolve root').stack;\n if (stack) {\n const lines = stack.split('\\n');\n for (const line of lines) {\n const match = line.match(/at\\s+(?:.*?\\()?(?:file:\\/\\/)?([^:)]+):\\d+:\\d+/);\n if (!match) {\n continue;\n }\n\n const filePath = match[1];\n if (filePath.includes('node_modules') || filePath.includes('/src/spec/')) {\n continue;\n }\n\n return resolve(filePath, '..', root);\n }\n }\n\n return resolve(process.cwd(), root);\n}\n\n/**\n * Resolve a CLI command — checks node_modules/.bin, then treats as absolute/PATH.\n */\nexport function resolveCommand(command: string, root: string): string {\n if (isAbsolute(command)) {\n return command;\n }\n\n const binPath = resolve(root, 'node_modules/.bin', command);\n if (existsSync(binPath)) {\n return binPath;\n }\n\n const cwdBinPath = resolve(process.cwd(), 'node_modules/.bin', command);\n if (existsSync(cwdBinPath)) {\n return cwdBinPath;\n }\n\n return command;\n}\n","import type { DatabasePort } from '../adapters/ports/database.port.js';\nimport { ExecAdapter } from '../builder/cli/adapters/exec.adapter.js';\nimport { FetchAdapter } from '../builder/http/adapters/fetch.adapter.js';\nimport { HonoAdapter } from '../builder/http/adapters/hono.adapter.js';\nimport {\n createSpecificationRunner,\n type SpecificationBuilder,\n} from '../builder/specification-builder.js';\nimport { dockerContainer } from '../docker/docker-adapter.js';\nimport { DockerAssertion } from '../docker/docker-assertion.js';\nimport { Orchestrator } from '../infra/orchestrator.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport { resolveCommand, resolveProjectRoot } from './resolve.js';\nimport type { AppTarget, CommandTarget, SpecTarget, StackTarget } from './targets.js';\n\n/** Shared options for all spec targets. */\nexport interface SpecOptions {\n /** Project root for fixture lookup and compose detection (relative paths supported). */\n root?: string;\n /** Infrastructure services to start via testcontainers. */\n services?: ServiceHandle[];\n}\n\n/** A specification runner with teardown support and orchestrator access. */\nexport interface SpecRunner {\n /** The function to create individual specs: `runner('label').get('/path').run()`. */\n (label: string): SpecificationBuilder;\n /** Stop all infrastructure started by this runner. */\n cleanup: () => Promise<void>;\n /**\n * Get a Docker assertion builder for a running container.\n *\n * @example\n * await runner.docker('my-container').toBeRunning();\n */\n docker: (containerId: string) => DockerAssertion;\n /** The orchestrator managing the test infrastructure lifecycle. */\n orchestrator: Orchestrator;\n}\n\n/**\n * Create a specification runner for the given target.\n *\n * @param target - What to test against: {@link app}, {@link stack}, or {@link command}.\n * @param options - Shared options: root directory, infrastructure services.\n *\n * @example\n * // HTTP — in-process app with testcontainers\n * const s = await spec(app(() => createApp(db)), { services: [db] });\n *\n * // HTTP — full docker compose stack\n * const s = await spec(stack('../../'));\n *\n * // CLI — command binary\n * const s = await spec(command('my-cli'), { root: '../fixtures' });\n */\nexport async function spec(target: SpecTarget, options: SpecOptions = {}): Promise<SpecRunner> {\n switch (target.kind) {\n case 'app': {\n return startApp(target, options);\n }\n case 'stack': {\n return startStack(target, options);\n }\n case 'command': {\n return startCommand(target, options);\n }\n }\n}\n\n// ── Isolation helpers ──\n\nfunction getWorkerId(): string {\n return process.env.VITEST_POOL_ID ?? '0';\n}\n\nasync function acquireIsolation(services: ServiceHandle[]): Promise<void> {\n const workerId = getWorkerId();\n for (const service of services) {\n await service.isolation().acquire(workerId);\n }\n}\n\nasync function releaseIsolation(services: ServiceHandle[]): Promise<void> {\n for (const service of services) {\n await service.isolation().release();\n }\n}\n\n// ── Internal dispatchers ──\n\nasync function startApp(target: AppTarget, options: SpecOptions): Promise<SpecRunner> {\n const services = options.services ?? [];\n const orchestrator = new Orchestrator({\n mode: 'integration',\n root: resolveProjectRoot(options.root),\n services,\n });\n\n await orchestrator.start();\n await acquireIsolation(services);\n\n // Build services map keyed by composeName or type\n const servicesMap: Record<string, ServiceHandle> = {};\n for (const svc of services) {\n const key = svc.composeName ?? svc.type;\n servicesMap[key] = svc;\n }\n\n const factoryResult = target.factory(servicesMap);\n\n // Normalize: bare HonoApp or { server, jobs }\n // Check for 'jobs' array (not 'server') to discriminate — bare apps may have a 'server' property\n const isComposite = Array.isArray((factoryResult as any).jobs);\n const honoApp = isComposite ? (factoryResult as any).server : factoryResult;\n const jobs = isComposite ? (factoryResult as any).jobs : undefined;\n\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n jobs,\n server: new HonoAdapter(honoApp),\n }) as SpecRunner;\n\n runner.cleanup = async () => {\n await releaseIsolation(services);\n await orchestrator.stop();\n };\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n\nasync function startStack(target: StackTarget, options: SpecOptions): Promise<SpecRunner> {\n const root = resolveProjectRoot(target.root ?? options.root);\n const workerId = getWorkerId();\n const projectName = `test-worker-${workerId}`;\n\n const orchestrator = new Orchestrator({\n mode: 'e2e',\n root,\n services: options.services ?? [],\n projectName,\n });\n\n await orchestrator.startCompose();\n\n const appUrl = orchestrator.getAppUrl();\n if (!appUrl) {\n throw new Error(\n 'stack(): could not detect app URL from compose. Ensure an app service with ports is defined.',\n );\n }\n\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n server: new FetchAdapter(appUrl),\n }) as SpecRunner;\n\n runner.cleanup = () => orchestrator.stopCompose();\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n\nasync function startCommand(target: CommandTarget, options: SpecOptions): Promise<SpecRunner> {\n const root = resolveProjectRoot(options.root);\n const bin = resolveCommand(target.bin, root);\n const services = options.services ?? [];\n\n let orchestrator: null | Orchestrator = null;\n let database: DatabasePort | undefined;\n let databases: Map<string, DatabasePort> | undefined;\n\n if (services.length) {\n orchestrator = new Orchestrator({\n mode: 'integration',\n root,\n services,\n });\n await orchestrator.start();\n await acquireIsolation(services);\n database = orchestrator.getDatabase() ?? undefined;\n const dbMap = orchestrator.getDatabases();\n databases = dbMap.size > 0 ? dbMap : undefined;\n }\n\n const runner = createSpecificationRunner({\n command: new ExecAdapter(bin),\n database,\n databases,\n fixturesRoot: root,\n }) as SpecRunner;\n\n runner.cleanup = async () => {\n await releaseIsolation(services);\n if (orchestrator) {\n await orchestrator.stop();\n }\n };\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator!;\n\n return runner;\n}\n","/**\n * Target factories for {@link spec}. Each target describes what is being tested\n * and how the specification runner should connect to it.\n */\n\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\n\n/** Any object with a request method compatible with Hono's app.request(). */\ntype HonoApp = {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n};\n\n/** A named job that can be triggered via .job(). */\nexport interface JobHandle {\n name: string;\n execute: () => Promise<void>;\n}\n\n/** Services map passed to the app factory after infrastructure starts. */\nexport type AppServices = Record<string, ServiceHandle>;\n\n/**\n * Return value from the app() factory. Either a bare server (backward compat)\n * or an object with server + jobs for job-based testing.\n */\nexport type AppFactoryResult = HonoApp | { server: HonoApp; jobs: JobHandle[] };\n\n// ── Target types ──\n\n/** In-process Hono app target. Created by {@link app}. */\nexport interface AppTarget {\n readonly kind: 'app';\n readonly factory: (services: AppServices) => AppFactoryResult;\n}\n\n/** Docker compose stack target. Created by {@link stack}. */\nexport interface StackTarget {\n readonly kind: 'stack';\n readonly root: string;\n}\n\n/** CLI command target. Created by {@link command}. */\nexport interface CommandTarget {\n readonly kind: 'command';\n readonly bin: string;\n}\n\n/** Any target that produces an HTTP interface (app or stack). */\nexport type HttpTarget = AppTarget | StackTarget;\n\n/** Any valid spec target. */\nexport type SpecTarget = AppTarget | CommandTarget | StackTarget;\n\n// ── Target factories ──\n\n/**\n * Test against an in-process Hono app. The factory receives started services\n * so you can wire connection strings into your app/DI container.\n *\n * @param factory - Function that receives services and returns a Hono app instance.\n *\n * @example\n * const db = postgres({ compose: 'db' });\n * await spec(\n * app((services) => createApp({ databaseUrl: services.db.connectionString })),\n * { services: [db] },\n * );\n */\nexport function app(factory: (services: AppServices) => HonoApp): AppTarget {\n return { kind: 'app', factory };\n}\n\n/**\n * Test against a full docker compose stack. The stack is started with\n * `docker compose up` and real HTTP requests are sent to the app service.\n *\n * @param root - Project root containing `docker/compose.test.yaml`.\n *\n * @example\n * await spec(stack('../../'));\n */\nexport function stack(root: string): StackTarget {\n return { kind: 'stack', root };\n}\n\n/**\n * Test a CLI binary. Each spec runs in a fresh temp directory.\n *\n * @param bin - Path to the CLI binary or command name (resolved from node_modules/.bin or PATH).\n *\n * @example\n * await spec(command('my-cli'), { root: '../fixtures' });\n */\nexport function command(bin: string): CommandTarget {\n return { kind: 'command', bin };\n}\n","import type { DatabasePort } from '../adapters/ports/database.port.js';\nimport { ExecAdapter } from '../builder/cli/adapters/exec.adapter.js';\nimport { createSpecificationRunner } from '../builder/specification-builder.js';\nimport { Orchestrator } from '../infra/orchestrator.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport type { SpecificationRunnerWithCleanup } from './legacy-integration.js';\nimport { resolveCommand, resolveProjectRoot } from './resolve.js';\n\nexport interface CliOptions {\n /** CLI command to run (resolved from node_modules/.bin or PATH). */\n command: string;\n /** Project root — base dir for .project() fixture lookup (relative paths supported). */\n root?: string;\n /** Optional infrastructure services (started via testcontainers). */\n services?: ServiceHandle[];\n}\n\n/**\n * Create a CLI specification runner.\n * Runs CLI commands against fixture projects. Optionally starts infrastructure.\n */\nexport async function cli(options: CliOptions): Promise<SpecificationRunnerWithCleanup> {\n const root = resolveProjectRoot(options.root);\n const command = resolveCommand(options.command, root);\n\n let orchestrator: null | Orchestrator = null;\n let database: DatabasePort | undefined;\n let databases: Map<string, DatabasePort> | undefined;\n\n if (options.services?.length) {\n orchestrator = new Orchestrator({\n mode: 'integration',\n root,\n services: options.services,\n });\n await orchestrator.start();\n database = orchestrator.getDatabase() ?? undefined;\n const dbMap = orchestrator.getDatabases();\n databases = dbMap.size > 0 ? dbMap : undefined;\n }\n\n const runner = createSpecificationRunner({\n command: new ExecAdapter(command),\n database,\n databases,\n fixturesRoot: root,\n }) as SpecificationRunnerWithCleanup;\n\n runner.cleanup = async () => {\n if (orchestrator) {\n await orchestrator.stop();\n }\n };\n runner.orchestrator = orchestrator!;\n\n return runner;\n}\n","import { FetchAdapter } from '../builder/http/adapters/fetch.adapter.js';\nimport { createSpecificationRunner } from '../builder/specification-builder.js';\nimport { Orchestrator } from '../infra/orchestrator.js';\nimport type { SpecificationRunnerWithCleanup } from './legacy-integration.js';\nimport { resolveProjectRoot } from './resolve.js';\n\nexport interface E2eOptions {\n /** Project root — must contain docker/compose.test.yaml. */\n root?: string;\n}\n\n/**\n * Create an E2E specification runner.\n * Starts full docker compose stack. App URL and database auto-detected.\n */\nexport async function e2e(options: E2eOptions = {}): Promise<SpecificationRunnerWithCleanup> {\n const orchestrator = new Orchestrator({\n mode: 'e2e',\n root: resolveProjectRoot(options.root),\n services: [],\n });\n\n await orchestrator.startCompose();\n\n const appUrl = orchestrator.getAppUrl();\n if (!appUrl) {\n throw new Error(\n 'E2E: could not detect app URL from compose. Ensure an app service with ports is defined.',\n );\n }\n\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n server: new FetchAdapter(appUrl),\n }) as SpecificationRunnerWithCleanup;\n\n runner.cleanup = () => orchestrator.stopCompose();\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n","import { HonoAdapter } from '../builder/http/adapters/hono.adapter.js';\nimport {\n createSpecificationRunner,\n type SpecificationRunner,\n} from '../builder/specification-builder.js';\nimport { Orchestrator } from '../infra/orchestrator.js';\nimport type { ServiceHandle } from '../infra/ports/service.port.js';\nimport { resolveProjectRoot } from './resolve.js';\n\ntype HonoApp = {\n fetch: (...args: any[]) => any;\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n};\n\nexport interface IntegrationOptions {\n /** Factory that returns a Hono app — called after services start. */\n app: () => HonoApp;\n /** Project root for compose detection (relative paths supported). */\n root?: string;\n /** Declared services — started via testcontainers. */\n services: ServiceHandle[];\n}\n\n/** A specification runner that also exposes teardown and the underlying orchestrator. */\nexport interface SpecificationRunnerWithCleanup extends SpecificationRunner {\n /** Stop all infrastructure containers started by this runner. */\n cleanup: () => Promise<void>;\n /** The orchestrator managing the test infrastructure lifecycle. */\n orchestrator: Orchestrator;\n}\n\n/**\n * Create an integration specification runner.\n * Starts infra containers via testcontainers, app runs in-process.\n */\nexport async function integration(\n options: IntegrationOptions,\n): Promise<SpecificationRunnerWithCleanup> {\n const orchestrator = new Orchestrator({\n mode: 'integration',\n root: resolveProjectRoot(options.root),\n services: options.services,\n });\n\n await orchestrator.start();\n\n const app = options.app();\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n server: new HonoAdapter(app),\n }) as SpecificationRunnerWithCleanup;\n\n runner.cleanup = () => orchestrator.stop();\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["postgres","redis","sep","shouldUpdateSnapshots","shouldUpdateSnapshots","shouldUpdateSnapshots","grepUtil"],"sources":["../src/infra/docker/docker-assertion.ts","../src/infra/docker/docker.ts","../src/spec/reporter.ts","../src/infra/containers/compose-parser.ts","../src/infra/containers/compose.ts","../src/infra/containers/testcontainers.ts","../src/infra/orchestrator.ts","../src/spec/result/directory.ts","../src/spec/result/filesystem.ts","../src/spec/result/grep.ts","../src/spec/result/json.ts","../src/spec/result/table.ts","../src/spec/result/result.ts","../src/spec/result/stream.ts","../src/spec/modes/cli/container-accessor.ts","../src/spec/modes/cli/docker-lookup.ts","../src/spec/modes/cli/result.ts","../src/spec/result/response.ts","../src/spec/modes/http/result.ts","../src/spec/builder.ts","../src/spec/modes/cli/adapters/exec.adapter.ts","../src/spec/modes/http/adapters/fetch.adapter.ts","../src/spec/modes/http/adapters/hono.adapter.ts","../src/spec/resolve.ts","../src/spec/spec.ts","../src/spec/targets.ts"],"sourcesContent":["import type { DockerContainerPort } from './docker.port.js';\n\n/**\n * Fluent assertion builder for Docker containers.\n * Chain async assertions like `toBeRunning()`, `toHaveFile()`, `toHaveMount()`.\n */\nexport class DockerAssertion {\n private container: DockerContainerPort;\n\n constructor(container: DockerContainerPort) {\n this.container = container;\n }\n\n /** Assert the container is running */\n async toBeRunning(): Promise<this> {\n const running = await this.container.isRunning();\n if (!running) {\n throw new Error('Expected container to be running');\n }\n return this;\n }\n\n /** Assert the container is NOT running / doesn't exist */\n async toNotExist(): Promise<this> {\n const running = await this.container.isRunning();\n if (running) {\n throw new Error('Expected container to not exist or not be running');\n }\n return this;\n }\n\n /** Assert a file exists inside the container */\n async toHaveFile(path: string, opts?: { containing?: string }): Promise<this> {\n const file = await this.container.file(path);\n if (!file.exists) {\n throw new Error(`Expected file ${path} to exist in container`);\n }\n if (opts?.containing && !file.content.includes(opts.containing)) {\n throw new Error(\n `Expected file ${path} to contain \"${opts.containing}\", got: ${file.content.slice(0, 200)}`,\n );\n }\n return this;\n }\n\n /** Assert a file does NOT exist */\n async toNotHaveFile(path: string): Promise<this> {\n const exists = await this.container.exists(path);\n if (exists) {\n throw new Error(`Expected ${path} to not exist in container`);\n }\n return this;\n }\n\n /** Assert a directory exists */\n async toHaveDirectory(path: string): Promise<this> {\n const exists = await this.container.exists(path);\n if (!exists) {\n throw new Error(`Expected directory ${path} to exist in container`);\n }\n return this;\n }\n\n /** Assert a mount exists */\n async toHaveMount(destination: string): Promise<this> {\n const info = await this.container.inspect();\n const mount = info.hostConfig.mounts.find((m) => m.destination === destination);\n if (!mount) {\n const available = info.hostConfig.mounts.map((m) => m.destination).join(', ');\n throw new Error(`Expected mount at ${destination}, found: [${available}]`);\n }\n return this;\n }\n\n /** Assert network mode */\n async toHaveNetwork(mode: string): Promise<this> {\n const info = await this.container.inspect();\n if (!info.hostConfig.networkMode.includes(mode)) {\n throw new Error(\n `Expected network mode \"${mode}\", got \"${info.hostConfig.networkMode}\"`,\n );\n }\n return this;\n }\n\n /** Assert memory limit */\n async toHaveMemoryLimit(bytes: number): Promise<this> {\n const info = await this.container.inspect();\n if (info.hostConfig.memory !== bytes) {\n throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);\n }\n return this;\n }\n\n /** Assert CPU quota */\n async toHaveCpuQuota(quota: number): Promise<this> {\n const info = await this.container.inspect();\n if (info.hostConfig.cpuQuota !== quota) {\n throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);\n }\n return this;\n }\n\n /** Execute a command and return output for custom assertions */\n async exec(cmd: string[]): Promise<string> {\n return this.container.exec(cmd);\n }\n\n /** Read a file for custom assertions */\n async readFile(path: string): Promise<string> {\n const file = await this.container.file(path);\n if (!file.exists) {\n throw new Error(`File ${path} does not exist`);\n }\n return file.content;\n }\n\n /** Get logs for custom assertions */\n async getLogs(tail?: number): Promise<string> {\n return this.container.logs(tail);\n }\n}\n","import { execSync } from 'node:child_process';\n\nimport type { DockerContainerPort, DockerInspectResult } from './docker.port.js';\n\n/**\n * Implements {@link DockerContainerPort} by shelling out to the Docker CLI.\n * Each instance is bound to a single container ID.\n */\nexport class DockerAdapter implements DockerContainerPort {\n private containerId: string;\n\n constructor(containerId: string) {\n this.containerId = containerId;\n }\n\n async exec(cmd: string[]): Promise<string> {\n const result = execSync(\n `docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(' ')}`,\n { encoding: 'utf8', timeout: 10_000 },\n );\n return result.trim();\n }\n\n async file(path: string): Promise<{ exists: boolean; content: string }> {\n try {\n const content = await this.exec(['cat', path]);\n return { exists: true, content };\n } catch {\n return { exists: false, content: '' };\n }\n }\n\n async isRunning(): Promise<boolean> {\n try {\n const result = execSync(\n `docker inspect --format='{{.State.Running}}' ${this.containerId}`,\n {\n encoding: 'utf8',\n timeout: 5000,\n },\n );\n return result.trim() === 'true';\n } catch {\n return false;\n }\n }\n\n async logs(tail?: number): Promise<string> {\n const tailFlag = tail ? `--tail ${tail}` : '';\n const result = execSync(`docker logs ${tailFlag} ${this.containerId}`, {\n encoding: 'utf8',\n timeout: 10_000,\n });\n return result;\n }\n\n async inspect(): Promise<DockerInspectResult> {\n const raw = execSync(`docker inspect ${this.containerId}`, {\n encoding: 'utf8',\n timeout: 5000,\n });\n const data = JSON.parse(raw)[0];\n return {\n id: data.Id,\n name: data.Name,\n state: {\n running: data.State.Running,\n exitCode: data.State.ExitCode,\n status: data.State.Status,\n },\n config: {\n image: data.Config.Image,\n env: data.Config.Env || [],\n },\n hostConfig: {\n memory: data.HostConfig.Memory || 0,\n cpuQuota: data.HostConfig.CpuQuota || 0,\n networkMode: data.HostConfig.NetworkMode || '',\n mounts: (data.Mounts || []).map((m: any) => ({\n source: m.Source,\n destination: m.Destination,\n type: m.Type,\n })),\n },\n networkSettings: {\n networks: Object.fromEntries(\n Object.entries(data.NetworkSettings.Networks || {}).map(\n ([name, net]: [string, any]) => [\n name,\n { gateway: net.Gateway, ipAddress: net.IPAddress },\n ],\n ),\n ),\n },\n };\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await this.exec(['test', '-e', path]);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Create a {@link DockerContainerPort} bound to an existing container by ID. */\nexport function dockerContainer(containerId: string): DockerContainerPort {\n return new DockerAdapter(containerId);\n}\n","// ── Colors ──\n\nconst GREEN = '\\x1b[32m';\nconst RED = '\\x1b[31m';\nconst DIM = '\\x1b[2m';\nconst BOLD = '\\x1b[1m';\nconst RESET = '\\x1b[0m';\nconst BG_CYAN = '\\x1b[46m';\nconst BLACK = '\\x1b[30m';\n\n// ── Symbols (vitest-native) ──\n\nconst CHECK = '✓';\nconst CROSS = '×';\nconst ARROW = '→';\nconst DASH = '⎯';\n\n// ── Types ──\n\n/** Status report for a single infrastructure service after startup. */\nexport interface ServiceReport {\n name: string;\n type: string;\n connectionString?: string;\n durationMs: number;\n error?: string;\n logs?: string;\n}\n\n/** Describes the application under test for startup reporting. */\nexport interface AppInfo {\n type: 'http' | 'in-process';\n url?: string;\n}\n\n// ── Startup report ──\n\nexport function formatStartupReport(\n mode: 'e2e' | 'integration',\n services: ServiceReport[],\n app?: AppInfo,\n): string {\n const lines: string[] = [];\n\n lines.push('');\n lines.push(`${BG_CYAN}${BLACK}${BOLD} INFRA ${RESET} Starting infrastructure...`);\n lines.push('');\n\n for (const service of services) {\n if (service.error) {\n lines.push(\n ` ${RED}${CROSS}${RESET} ${service.type} (${service.name}) ${RED}${service.error}${RESET} ${DIM}${service.durationMs}ms${RESET}`,\n );\n if (service.logs) {\n const logLines = service.logs.trim().split('\\n').slice(-10);\n for (const logLine of logLines) {\n lines.push(` ${DIM}${logLine}${RESET}`);\n }\n }\n } else {\n const conn = service.connectionString\n ? `${DIM}${service.connectionString}${RESET}`\n : '';\n lines.push(\n ` ${GREEN}${CHECK}${RESET} ${service.type} (${service.name}) ${conn} ${DIM}${service.durationMs}ms${RESET}`,\n );\n }\n }\n\n if (app) {\n lines.push('');\n if (app.type === 'in-process') {\n lines.push(` ${DIM}${ARROW} app: in-process (Hono)${RESET}`);\n } else {\n lines.push(` ${DIM}${ARROW} app: ${app.url}${RESET}`);\n }\n }\n\n lines.push('');\n\n return lines.join('\\n');\n}\n\n// ── Error divider ──\n\nexport function formatErrorDivider(label: string): string {\n const dashes = DASH.repeat(30);\n return `\\n${RED}${dashes} ${label} ${dashes}${RESET}\\n`;\n}\n\n// ── Status error ──\n\nexport function formatStatusError(\n expectedStatus: number,\n receivedStatus: number,\n request: { method: string; path: string; body?: unknown },\n responseBody: unknown,\n): string {\n const lines: string[] = [];\n\n lines.push(`Expected status: ${GREEN}${expectedStatus}${RESET}`);\n lines.push(`Received status: ${RED}${receivedStatus}${RESET}`);\n lines.push('');\n lines.push(`${DIM}${request.method} ${request.path}${RESET}`);\n\n if (request.body) {\n lines.push(formatJson(request.body, DIM));\n }\n\n if (responseBody) {\n lines.push('');\n lines.push(`${DIM}Response:${RESET}`);\n lines.push(formatJson(responseBody, RED));\n }\n\n return lines.join('\\n');\n}\n\n// ── Table diff ──\n\nexport function formatTableDiff(\n table: string,\n columns: string[],\n expected: unknown[][],\n actual: unknown[][],\n): string {\n const lines: string[] = [];\n\n lines.push(`Table \"${table}\" mismatch`);\n lines.push(`${DIM} query: ${columns.join(', ')}${RESET}`);\n lines.push(`${DIM} expected: ${rowLabel(expected.length)}${RESET}`);\n lines.push(`${DIM} received: ${rowLabel(actual.length)}${RESET}`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const header = columns.join(' | ');\n lines.push(`${DIM} ${header}${RESET}`);\n\n const maxRows = Math.max(expected.length, actual.length);\n\n for (let i = 0; i < maxRows; i++) {\n const exp = expected[i];\n const act = actual[i];\n\n if (exp && !act) {\n lines.push(`${GREEN}- ${formatRow(exp)}${RESET}`);\n } else if (!exp && act) {\n lines.push(`${RED}+ ${formatRow(act)}${RESET}`);\n } else if (exp && act) {\n const same = JSON.stringify(exp) === JSON.stringify(act);\n if (same) {\n lines.push(` ${formatRow(act)}`);\n } else {\n lines.push(`${GREEN}- ${formatRow(exp)}${RESET}`);\n lines.push(`${RED}+ ${formatRow(act)}${RESET}`);\n }\n }\n }\n\n if (expected.length === 0 && actual.length === 0) {\n lines.push(` (empty)`);\n }\n\n return lines.join('\\n');\n}\n\n// ── Response diff ──\n\nexport function formatResponseDiff(file: string, expected: unknown, actual: unknown): string {\n const lines: string[] = [];\n\n lines.push(`Response mismatch (${file})`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const expectedLines = JSON.stringify(expected, null, 2).split('\\n');\n const actualLines = JSON.stringify(actual, null, 2).split('\\n');\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n\n if (exp === act) {\n lines.push(` ${exp}`);\n } else {\n if (exp !== undefined) {\n lines.push(`${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(`${RED}+ ${act}${RESET}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Exit code error ──\n\nexport function formatExitCodeError(\n expected: number,\n received: number,\n stdout: string,\n stderr: string,\n): string {\n const lines: string[] = [];\n\n lines.push(`Expected exit code: ${GREEN}${expected}${RESET}`);\n lines.push(`Received exit code: ${RED}${received}${RESET}`);\n\n if (stdout.trim()) {\n lines.push('');\n lines.push(`${DIM}stdout:${RESET}`);\n for (const line of stdout.trim().split('\\n').slice(-15)) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n }\n\n if (stderr.trim()) {\n lines.push('');\n lines.push(`${DIM}stderr:${RESET}`);\n for (const line of stderr.trim().split('\\n').slice(-15)) {\n lines.push(` ${RED}${line}${RESET}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Stdout/stderr diff ──\n\nexport function formatStdoutDiff(file: string, expected: string, actual: string): string {\n const lines: string[] = [];\n\n lines.push(`Output mismatch (${file})`);\n lines.push('');\n lines.push(`${GREEN}- Expected${RESET}`);\n lines.push(`${RED}+ Received${RESET}`);\n lines.push('');\n\n const expectedLines = expected.split('\\n');\n const actualLines = actual.split('\\n');\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n\n if (exp === act) {\n lines.push(` ${exp}`);\n } else {\n if (exp !== undefined) {\n lines.push(`${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(`${RED}+ ${act}${RESET}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Directory diff ──\n\ninterface DirectoryDiffData {\n added: string[];\n changed: { path: string; expected: string; actual: string }[];\n removed: string[];\n}\n\nexport function formatDirectoryDiff(\n fixtureName: string,\n diff: DirectoryDiffData,\n hint: string,\n): string {\n const lines: string[] = [];\n\n const total = diff.added.length + diff.removed.length + diff.changed.length;\n lines.push(`Directory mismatch: ${BOLD}${fixtureName}${RESET}`);\n lines.push(\n `${DIM} ${total} difference${total === 1 ? '' : 's'}: ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed${RESET}`,\n );\n lines.push('');\n lines.push(`${GREEN}- Expected (fixture)${RESET}`);\n lines.push(`${RED}+ Received (generated)${RESET}`);\n lines.push('');\n\n for (const path of diff.added) {\n lines.push(`${RED}+ added ${path}${RESET} ${DIM}(not in fixture)${RESET}`);\n }\n for (const path of diff.removed) {\n lines.push(`${GREEN}- removed ${path}${RESET} ${DIM}(in fixture, not generated)${RESET}`);\n }\n for (const { path, expected, actual } of diff.changed) {\n const expectedLines = expected.split('\\n');\n const actualLines = actual.split('\\n');\n const changedCount = countLineDifferences(expectedLines, actualLines);\n lines.push(\n `${BOLD}~ changed ${path}${RESET} ${DIM}(${changedCount} line${changedCount === 1 ? '' : 's'} differ)${RESET}`,\n );\n\n let shown = 0;\n const maxShown = 5;\n const maxLines = Math.max(expectedLines.length, actualLines.length);\n for (let i = 0; i < maxLines && shown < maxShown; i++) {\n const exp = expectedLines[i];\n const act = actualLines[i];\n if (exp !== act) {\n lines.push(`${DIM} line ${i + 1}:${RESET}`);\n if (exp !== undefined) {\n lines.push(` ${GREEN}- ${exp}${RESET}`);\n }\n if (act !== undefined) {\n lines.push(` ${RED}+ ${act}${RESET}`);\n }\n shown++;\n }\n }\n if (changedCount > maxShown) {\n lines.push(` ${DIM}... ${changedCount - maxShown} more line(s)${RESET}`);\n }\n }\n\n lines.push('');\n lines.push(`${DIM}${hint}${RESET}`);\n\n return lines.join('\\n');\n}\n\nfunction countLineDifferences(expected: string[], actual: string[]): number {\n let count = 0;\n const max = Math.max(expected.length, actual.length);\n for (let i = 0; i < max; i++) {\n if (expected[i] !== actual[i]) {\n count++;\n }\n }\n return count;\n}\n\n// ── File assertions ──\n\nexport function formatFileMissing(path: string): string {\n return `Expected file to exist: ${RED}${path}${RESET}`;\n}\n\nexport function formatFileUnexpected(path: string): string {\n return `Expected file NOT to exist: ${RED}${path}${RESET}`;\n}\n\nexport function formatFileContentMismatch(path: string, expected: string, actual: string): string {\n const lines: string[] = [];\n lines.push(`File \"${path}\" does not contain expected content`);\n lines.push('');\n lines.push(`${GREEN}Expected to contain:${RESET}`);\n lines.push(` ${GREEN}${expected}${RESET}`);\n lines.push('');\n lines.push(`${RED}Actual content (first 20 lines):${RESET}`);\n for (const line of actual.split('\\n').slice(0, 20)) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n return lines.join('\\n');\n}\n\n// ── Service logs section ──\n\nexport function formatServiceLogs(services: { name: string; logs: string }[]): string {\n const lines: string[] = [];\n\n for (const { name, logs } of services) {\n if (!logs.trim()) {\n continue;\n }\n\n lines.push('');\n lines.push(`${DIM}${name} logs (last 10 lines):${RESET}`);\n\n const logLines = logs.trim().split('\\n').slice(-10);\n for (const line of logLines) {\n lines.push(` ${DIM}${line}${RESET}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n// ── Helpers ──\n\nfunction rowLabel(n: number): string {\n return n === 1 ? '1 row' : `${n} rows`;\n}\n\nfunction formatJson(value: unknown, color: string): string {\n return JSON.stringify(value, null, 2)\n .split('\\n')\n .map((line) => `${color}${line}${RESET}`)\n .join('\\n');\n}\n\nfunction formatRow(row: unknown[]): string {\n return row.map((v) => String(v ?? 'null')).join(' | ');\n}\n\n// ── Test utilities ──\n\n/** Strip ANSI escape codes from a string. */\nexport function stripAnsi(str: string): string {\n // eslint-disable-next-line no-control-regex\n return str.replace(/\\x1b\\[[0-9;]*m/g, '');\n}\n\n/** Strip ANSI codes and replace volatile tokens (ports, durations) with placeholders. */\nexport function normalizeOutput(str: string): string {\n return stripAnsi(str)\n .replace(/localhost:\\d+/g, 'localhost:PORT')\n .replace(/\\d+ms/g, 'Xms')\n .replace(/\\d+\\.\\d+s/g, 'X.Xs')\n .trim();\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\n/**\n * A parsed service from docker-compose.test.yaml.\n */\nexport interface ComposeService {\n name: string;\n image?: string;\n build?: string;\n ports: { container: number; host?: number }[];\n environment: Record<string, string>;\n volumes: string[];\n dependsOn: string[];\n}\n\n/**\n * Result of parsing a compose file.\n */\nexport interface ComposeConfig {\n services: ComposeService[];\n appService: ComposeService | null;\n infraServices: ComposeService[];\n}\n\n/**\n * Detect the service type from the image name.\n */\nexport function detectServiceType(\n image: string | undefined,\n): 'app' | 'postgres' | 'redis' | 'unknown' {\n if (!image) {\n return 'app';\n }\n\n const lower = image.toLowerCase();\n\n if (lower.startsWith('postgres')) {\n return 'postgres';\n }\n if (lower.startsWith('redis')) {\n return 'redis';\n }\n\n return 'unknown';\n}\n\n/**\n * Find the compose file in the project.\n * Looks for docker/compose.test.yaml or docker-compose.test.yaml.\n */\nexport function findComposeFile(projectRoot: string): null | string {\n const candidates = [\n resolve(projectRoot, 'docker/compose.test.yaml'),\n resolve(projectRoot, 'docker/compose.test.yml'),\n resolve(projectRoot, 'docker-compose.test.yaml'),\n resolve(projectRoot, 'docker-compose.test.yml'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Parse a docker-compose file and extract service definitions.\n */\nexport function parseComposeFile(filePath: string): ComposeConfig {\n const content = readFileSync(filePath, 'utf8');\n const doc = parseYaml(content);\n\n if (!doc?.services) {\n return { services: [], appService: null, infraServices: [] };\n }\n\n const services: ComposeService[] = Object.entries(doc.services).map(\n ([name, def]: [string, any]) => {\n const ports: { container: number; host?: number }[] = [];\n if (def.ports) {\n for (const port of def.ports) {\n const str = String(port);\n if (str.includes(':')) {\n const [host, container] = str.split(':');\n ports.push({ container: Number(container), host: Number(host) });\n } else {\n ports.push({ container: Number(str) });\n }\n }\n }\n\n const environment: Record<string, string> = {};\n if (def.environment) {\n if (Array.isArray(def.environment)) {\n for (const env of def.environment) {\n const [key, ...rest] = String(env).split('=');\n environment[key] = rest.join('=');\n }\n } else {\n Object.assign(environment, def.environment);\n }\n }\n\n const volumes: string[] = def.volumes ? def.volumes.map((v: string) => String(v)) : [];\n\n let dependsOn: string[] = [];\n if (def.depends_on) {\n dependsOn = Array.isArray(def.depends_on)\n ? def.depends_on\n : Object.keys(def.depends_on);\n }\n\n return {\n name,\n image: def.image,\n build: def.build,\n ports,\n environment,\n volumes,\n dependsOn,\n };\n },\n );\n\n const appService = services.find((s) => s.build !== undefined) ?? null;\n const infraServices = services.filter((s) => s.build === undefined);\n\n return { services, appService, infraServices };\n}\n","import { execSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\nimport type { ContainerPort } from './container.port.js';\n\n/**\n * Container adapter using docker compose — runs full compose stack.\n * Used by e2e() to start all services including the app.\n */\nexport class ComposeAdapter implements ContainerPort {\n private composeFile: string;\n private serviceName: string;\n private started = false;\n\n constructor(composeFile: string, serviceName: string) {\n this.composeFile = composeFile;\n this.serviceName = serviceName;\n }\n\n private exec(command: string): string {\n return execSync(command, {\n cwd: dirname(this.composeFile),\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'inherit'],\n }).trim();\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n this.exec(`docker compose -f ${this.composeFile} up -d --wait ${this.serviceName}`);\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n this.exec(`docker compose -f ${this.composeFile} rm -fsv ${this.serviceName}`);\n this.started = false;\n }\n\n getMappedPort(containerPort: number): number {\n const output = this.exec(\n `docker compose -f ${this.composeFile} port ${this.serviceName} ${containerPort}`,\n );\n // Output: 0.0.0.0:54321\n const port = output.split(':').pop();\n return Number(port);\n }\n\n getHost(): string {\n return 'localhost';\n }\n\n getConnectionString(): string {\n return `${this.getHost()}:${this.getMappedPort(0)}`;\n }\n\n async getLogs(): Promise<string> {\n try {\n return this.exec(\n `docker compose -f ${this.composeFile} logs ${this.serviceName} --tail=50`,\n );\n } catch {\n return '';\n }\n }\n}\n\n/**\n * Start the full compose stack and stop it all on cleanup.\n * Supports per-worker project names for parallel execution.\n */\nexport class ComposeStackAdapter {\n private composeFile: string;\n private projectName: null | string;\n private started = false;\n\n constructor(composeFile: string, projectName?: string) {\n this.composeFile = composeFile;\n this.projectName = projectName ?? null;\n }\n\n private get projectFlag(): string {\n return this.projectName ? `-p ${this.projectName}` : '';\n }\n\n private run(command: string): string {\n try {\n return execSync(command, {\n cwd: dirname(this.composeFile),\n encoding: 'utf8',\n timeout: 120_000,\n }).trim();\n } catch (error: any) {\n const stderr = error.stderr?.toString().trim() ?? error.message;\n throw new Error(`docker compose failed: ${stderr}`, { cause: error });\n }\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} up -d --wait`);\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return;\n }\n\n this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} down -v`);\n this.started = false;\n }\n\n getMappedPort(serviceName: string, containerPort: number): number {\n const output = this.run(\n `docker compose ${this.projectFlag} -f ${this.composeFile} port ${serviceName} ${containerPort}`,\n );\n const port = output.split(':').pop();\n return Number(port);\n }\n\n getHost(): string {\n return 'localhost';\n }\n}\n","import type { ContainerPort } from './container.port.js';\n\n/**\n * Container adapter using testcontainers.\n * Wraps a GenericContainer for programmatic container lifecycle.\n */\nexport class TestcontainersAdapter implements ContainerPort {\n private image: string;\n private containerPort: number;\n private env: Record<string, string>;\n private reuse: boolean;\n private container: any = null;\n\n constructor(options: {\n image: string;\n port: number;\n env?: Record<string, string>;\n reuse?: boolean;\n }) {\n this.image = options.image;\n this.containerPort = options.port;\n this.env = options.env ?? {};\n this.reuse = options.reuse ?? false;\n }\n\n async start(): Promise<void> {\n const { GenericContainer, Wait } = await import('testcontainers');\n\n let builder = new GenericContainer(this.image).withExposedPorts(this.containerPort);\n\n for (const [key, value] of Object.entries(this.env)) {\n builder = builder.withEnvironment({ [key]: value });\n }\n\n if (this.image.startsWith('postgres')) {\n builder = builder.withWaitStrategy(\n Wait.forLogMessage(/database system is ready to accept connections/, 2),\n );\n }\n\n if (this.reuse) {\n builder = builder.withReuse();\n }\n\n this.container = await builder.start();\n }\n\n async stop(): Promise<void> {\n if (this.container && !this.reuse) {\n await this.container.stop();\n this.container = null;\n }\n }\n\n getMappedPort(containerPort: number): number {\n if (!this.container) {\n throw new Error('Container not started');\n }\n return this.container.getMappedPort(containerPort);\n }\n\n getHost(): string {\n if (!this.container) {\n throw new Error('Container not started');\n }\n return this.container.getHost();\n }\n\n getConnectionString(): string {\n return `${this.getHost()}:${this.getMappedPort(this.containerPort)}`;\n }\n\n async getLogs(): Promise<string> {\n if (!this.container) {\n return '';\n }\n\n const stream = await this.container.logs();\n return new Promise((resolve) => {\n let output = '';\n stream.on('data', (chunk: Buffer) => {\n output += chunk.toString();\n });\n stream.on('end', () => {\n resolve(output);\n });\n // Timeout after 1s if stream doesn't end\n setTimeout(() => {\n resolve(output);\n }, 1000);\n });\n }\n}\n","import { dirname } from 'node:path';\n\nimport { postgres } from '../services/postgres.js';\nimport { redis } from '../services/redis.js';\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\nimport { type AppInfo, formatStartupReport, type ServiceReport } from '../spec/reporter.js';\nimport {\n detectServiceType,\n findComposeFile,\n parseComposeFile,\n} from './containers/compose-parser.js';\nimport { ComposeStackAdapter } from './containers/compose.js';\nimport type { ContainerPort } from './containers/container.port.js';\nimport { TestcontainersAdapter } from './containers/testcontainers.js';\n\ninterface RunningService {\n handle: ServiceHandle;\n container: ContainerPort | null;\n}\n\nexport interface OrchestratorOptions {\n services: ServiceHandle[];\n mode: 'e2e' | 'integration';\n root?: string;\n /** Compose project name — used for per-worker stack isolation. */\n projectName?: string;\n}\n\n/**\n * Orchestrator for test infrastructure.\n * Integration: starts services via testcontainers.\n * E2E: runs full docker compose up.\n */\nexport class Orchestrator {\n private services: ServiceHandle[];\n private mode: 'e2e' | 'integration';\n private root: string;\n private projectName: string | undefined;\n private running: RunningService[] = [];\n private composeStack: ComposeStackAdapter | null = null;\n private composeHandles: ServiceHandle[] = [];\n private started = false;\n\n constructor(options: OrchestratorOptions) {\n this.services = options.services;\n this.mode = options.mode;\n this.root = options.root ?? process.cwd();\n this.projectName = options.projectName;\n }\n\n /**\n * Start declared services via testcontainers (integration mode).\n * Phase 1: start all containers in parallel (the slow part).\n * Phase 2: wire connections, healthcheck, and init sequentially (fast).\n */\n async start(): Promise<void> {\n if (this.started) {\n return;\n }\n\n const composePath = findComposeFile(this.root);\n const composeDir = composePath ? dirname(composePath) : this.root;\n const composeConfig = composePath ? parseComposeFile(composePath) : null;\n\n // Separate services that need containers from embedded ones (e.g. SQLite)\n const containerServices: { container: TestcontainersAdapter; handle: ServiceHandle }[] = [];\n const embeddedServices: ServiceHandle[] = [];\n\n for (const handle of this.services) {\n if (handle.defaultPort === 0) {\n // Embedded service (no container needed)\n embeddedServices.push(handle);\n continue;\n }\n\n let image = handle.defaultImage;\n let env = { ...handle.environment };\n\n if (handle.composeName && composeConfig) {\n const composeService = composeConfig.services.find(\n (s) => s.name === handle.composeName,\n );\n if (composeService) {\n image = composeService.image ?? image;\n env = { ...env, ...composeService.environment };\n Object.assign(handle.environment, composeService.environment);\n }\n }\n\n const container = new TestcontainersAdapter({ image, port: handle.defaultPort, env });\n containerServices.push({ container, handle });\n }\n\n // Phase 1: start containers in parallel + initialize embedded services\n await Promise.all([\n ...containerServices.map(({ container }) => container.start()),\n ...embeddedServices.map(async (handle) => {\n await handle.initialize(composeDir);\n handle.started = true;\n this.running.push({ handle, container: null });\n }),\n ]);\n\n // Phase 2: wire connections, healthcheck, init (fast — containers already running)\n const reports: ServiceReport[] = [];\n\n for (const { container, handle } of containerServices) {\n const serviceStartTime = Date.now();\n\n try {\n const host = container.getHost();\n const port = container.getMappedPort(handle.defaultPort);\n handle.connectionString = handle.buildConnectionString(host, port);\n\n await handle.healthcheck();\n await handle.initialize(composeDir);\n handle.started = true;\n\n reports.push({\n name: handle.composeName ?? handle.type,\n type: handle.type,\n connectionString: handle.connectionString,\n durationMs: Date.now() - serviceStartTime,\n });\n this.running.push({ handle, container });\n } catch (error: any) {\n let logs = '';\n try {\n logs = await container.getLogs();\n } catch {\n /* Ignore log fetch errors */\n }\n try {\n await container.stop();\n } catch {\n /* Ignore stop errors */\n }\n\n reports.push({\n name: handle.composeName ?? handle.type,\n type: handle.type,\n durationMs: Date.now() - serviceStartTime,\n error: error.message,\n logs,\n });\n\n const output = formatStartupReport('integration', reports, { type: 'in-process' });\n console.error(output);\n throw error;\n }\n }\n\n this.started = true;\n\n const appInfo: AppInfo = { type: 'in-process' };\n const output = formatStartupReport('integration', reports, appInfo);\n console.log(output);\n }\n\n /**\n * Stop testcontainers (integration mode).\n */\n async stop(): Promise<void> {\n for (const { container } of this.running) {\n if (container) {\n await container.stop();\n }\n }\n this.running = [];\n this.started = false;\n }\n\n /**\n * Start full docker compose stack (e2e mode).\n * Auto-detects infra services and creates handles for them.\n */\n async startCompose(): Promise<void> {\n const composePath = findComposeFile(this.root);\n if (!composePath) {\n throw new Error(`E2E: no compose file found in ${this.root}`);\n }\n\n const startTime = Date.now();\n const composeDir = dirname(composePath);\n const composeConfig = parseComposeFile(composePath);\n\n this.composeStack = new ComposeStackAdapter(composePath, this.projectName);\n await this.composeStack.start();\n\n // Create handles for detected infra services\n for (const service of composeConfig.infraServices) {\n const type = detectServiceType(service.image);\n\n if (type === 'postgres') {\n const handle = postgres({ compose: service.name, env: service.environment });\n const port = this.composeStack.getMappedPort(service.name, 5432);\n handle.connectionString = handle.buildConnectionString('localhost', port);\n\n await handle.initialize(composeDir);\n handle.started = true;\n\n this.composeHandles.push(handle);\n } else if (type === 'redis') {\n const handle = redis({ compose: service.name });\n const port = this.composeStack.getMappedPort(service.name, 6379);\n handle.connectionString = handle.buildConnectionString('localhost', port);\n handle.started = true;\n\n this.composeHandles.push(handle);\n }\n }\n\n const durationMs = Date.now() - startTime;\n const reports: ServiceReport[] = this.composeHandles.map((h) => ({\n name: h.composeName ?? h.type,\n type: h.type,\n connectionString: h.connectionString,\n durationMs,\n }));\n\n const appUrl = this.getAppUrl();\n const appInfo: AppInfo = { type: 'http', url: appUrl ?? undefined };\n const output = formatStartupReport('e2e', reports, appInfo);\n console.log(output);\n }\n\n /**\n * Stop docker compose stack (e2e mode).\n */\n async stopCompose(): Promise<void> {\n if (this.composeStack) {\n await this.composeStack.stop();\n this.composeStack = null;\n }\n this.composeHandles = [];\n }\n\n /**\n * Get a database service by compose name, or the first one if no name given.\n */\n getDatabase(serviceName?: string): DatabasePort | null {\n for (const handle of [...this.services, ...this.composeHandles]) {\n if (serviceName && handle.composeName !== serviceName) {\n continue;\n }\n const adapter = handle.createDatabaseAdapter();\n if (adapter) {\n return adapter;\n }\n }\n return null;\n }\n\n /**\n * Get all database services keyed by compose name.\n */\n getDatabases(): Map<string, DatabasePort> {\n const map = new Map<string, DatabasePort>();\n for (const handle of [...this.services, ...this.composeHandles]) {\n const adapter = handle.createDatabaseAdapter();\n if (adapter && handle.composeName) {\n map.set(handle.composeName, adapter);\n }\n }\n return map;\n }\n\n /**\n * Get app URL from compose (e2e mode).\n */\n getAppUrl(): null | string {\n const composePath = findComposeFile(this.root);\n if (!composePath || !this.composeStack) {\n return null;\n }\n\n const config = parseComposeFile(composePath);\n const appService = config.appService;\n\n if (!appService || appService.ports.length === 0) {\n return null;\n }\n\n const port = this.composeStack.getMappedPort(\n appService.name,\n appService.ports[0].container,\n );\n return `http://localhost:${port}`;\n }\n}\n","import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync } from 'node:fs';\nimport { readdir } from 'node:fs/promises';\nimport { relative, resolve, sep } from 'node:path';\n\nimport { formatDirectoryDiff } from '../reporter.js';\n\n// ── Directory walk + diff (internal helpers) ──\n\nconst DEFAULT_IGNORES = ['.git', '.DS_Store', 'node_modules', '.next', 'dist', '.turbo', '.cache'];\n\nexport interface DirectoryDiff {\n added: string[];\n changed: { path: string; expected: string; actual: string }[];\n removed: string[];\n}\n\n/**\n * Recursively walk a directory, returning sorted relative paths of files only.\n */\nexport async function walkDirectory(\n root: string,\n options: { ignore?: string[] } = {},\n): Promise<string[]> {\n const ignores = new Set([...DEFAULT_IGNORES, ...(options.ignore ?? [])]);\n const out: string[] = [];\n\n async function walk(current: string): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(current);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (ignores.has(entry)) {\n continue;\n }\n const abs = resolve(current, entry);\n const stat = statSync(abs);\n if (stat.isDirectory()) {\n await walk(abs);\n } else if (stat.isFile()) {\n out.push(relative(root, abs).split(sep).join('/'));\n }\n }\n }\n\n await walk(root);\n out.sort();\n return out;\n}\n\n/**\n * Compare two directory trees file-by-file.\n */\nexport async function diffDirectories(\n expectedRoot: string,\n actualRoot: string,\n options: { ignore?: string[] } = {},\n): Promise<DirectoryDiff> {\n const expectedFiles = await walkDirectory(expectedRoot, options);\n const actualFiles = await walkDirectory(actualRoot, options);\n const expectedSet = new Set(expectedFiles);\n const actualSet = new Set(actualFiles);\n\n const added = actualFiles.filter((f) => !expectedSet.has(f));\n const removed = expectedFiles.filter((f) => !actualSet.has(f));\n const changed: DirectoryDiff['changed'] = [];\n\n for (const file of expectedFiles) {\n if (!actualSet.has(file)) {\n continue;\n }\n const expected = readFileSync(resolve(expectedRoot, file), 'utf8');\n const actual = readFileSync(resolve(actualRoot, file), 'utf8');\n if (expected !== actual) {\n changed.push({ actual, expected, path: file });\n }\n }\n\n return { added, changed, removed };\n}\n\n// ── DirectoryAccessor (user-facing) ──\n\nexport interface DirectorySnapshotOptions {\n ignore?: string[];\n update?: boolean;\n}\n\nfunction shouldUpdateSnapshots(): boolean {\n if (process.env.JTERRAZZ_TEST_UPDATE === '1') {\n return true;\n }\n if (process.env.UPDATE_SNAPSHOTS === '1') {\n return true;\n }\n if (process.argv.includes('-u') || process.argv.includes('--update')) {\n return true;\n }\n return false;\n}\n\nexport class DirectoryAccessor {\n private absPath: string;\n private testDir: string;\n\n constructor(absPath: string, testDir: string) {\n this.absPath = absPath;\n this.testDir = testDir;\n }\n\n async toMatchFixture(name: string, options: DirectorySnapshotOptions = {}): Promise<void> {\n const fixtureDir = resolve(this.testDir, 'expected', name);\n const update = options.update ?? shouldUpdateSnapshots();\n\n if (update) {\n rmSync(fixtureDir, { force: true, recursive: true });\n mkdirSync(fixtureDir, { recursive: true });\n cpSync(this.absPath, fixtureDir, { recursive: true });\n return;\n }\n\n if (!existsSync(fixtureDir)) {\n throw new Error(\n `Directory fixture \"${name}\" does not exist at ${fixtureDir}.\\n` +\n `Run with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`,\n );\n }\n\n const diff: DirectoryDiff = await diffDirectories(fixtureDir, this.absPath, {\n ignore: options.ignore,\n });\n\n if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {\n return;\n }\n\n throw new Error(\n formatDirectoryDiff(\n name,\n diff,\n 'Run with JTERRAZZ_TEST_UPDATE=1 to update the fixture.',\n ),\n );\n }\n\n async files(options: { ignore?: string[] } = {}): Promise<string[]> {\n return walkDirectory(this.absPath, options);\n }\n}\n","import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { formatDirectoryDiff } from '../reporter.js';\nimport {\n diffDirectories,\n type DirectoryDiff,\n type DirectorySnapshotOptions,\n walkDirectory,\n} from './directory.js';\n\nfunction shouldUpdateSnapshots(): boolean {\n if (process.env.JTERRAZZ_TEST_UPDATE === '1') {\n return true;\n }\n if (process.env.UPDATE_SNAPSHOTS === '1') {\n return true;\n }\n if (process.argv.includes('-u') || process.argv.includes('--update')) {\n return true;\n }\n return false;\n}\n\n/**\n * Accessor for the whole temporary working directory used by a CLI spec.\n *\n * Generalization of {@link DirectoryAccessor} that snapshots the entire CLI\n * working directory into `<test-file-dir>/expected/filesystem/<name>/`.\n */\nexport class FilesystemAccessor {\n /** The absolute path of the temporary working directory. */\n readonly cwd: string;\n private readonly testDir: string;\n\n constructor(cwd: string, testDir: string) {\n this.cwd = cwd;\n this.testDir = testDir;\n }\n\n /** List all files (recursively) under the working directory, sorted. */\n async files(options: { ignore?: string[] } = {}): Promise<string[]> {\n return walkDirectory(this.cwd, options);\n }\n\n /**\n * Assert the working directory matches a convention-based fixture tree:\n * `<test-file-dir>/expected/filesystem/<name>/`.\n */\n async toMatch(name: string, options: DirectorySnapshotOptions = {}): Promise<void> {\n const fixtureDir = resolve(this.testDir, 'expected', 'filesystem', name);\n const update = options.update ?? shouldUpdateSnapshots();\n\n if (update) {\n rmSync(fixtureDir, { force: true, recursive: true });\n mkdirSync(fixtureDir, { recursive: true });\n cpSync(this.cwd, fixtureDir, { recursive: true });\n return;\n }\n\n if (!existsSync(fixtureDir)) {\n throw new Error(\n `Filesystem fixture \"${name}\" does not exist at ${fixtureDir}.\\n` +\n `Run with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`,\n );\n }\n\n const diff: DirectoryDiff = await diffDirectories(fixtureDir, this.cwd, {\n ignore: options.ignore,\n });\n\n if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {\n return;\n }\n\n throw new Error(\n formatDirectoryDiff(\n `filesystem/${name}`,\n diff,\n 'Run with JTERRAZZ_TEST_UPDATE=1 to update the fixture.',\n ),\n );\n }\n}\n","/**\n * Extract text blocks from output that contain a pattern.\n * Splits by blank lines (how linter/compiler output is structured),\n * returns only blocks matching the pattern.\n *\n * @example\n * expect(grep(result.stdout, \"unused-var.ts\")).toContain(\"no-unused-vars\")\n * expect(grep(result.stdout, \"valid/sorted.ts\")).not.toContain(\"sort-imports\")\n */\nexport function grep(output: string, pattern: string): string {\n // eslint-disable-next-line no-control-regex\n const clean = output.replace(/\\x1b\\[[0-9;]*m/g, '');\n const blocks = clean.split(/\\n\\s*\\n/);\n return blocks.filter((block) => block.includes(pattern)).join('\\n\\n');\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, isAbsolute, resolve } from 'node:path';\n\nimport { formatResponseDiff } from '../reporter.js';\n\nfunction shouldUpdateSnapshots(): boolean {\n if (process.env.JTERRAZZ_TEST_UPDATE === '1') {\n return true;\n }\n if (process.env.UPDATE_SNAPSHOTS === '1') {\n return true;\n }\n if (process.argv.includes('-u') || process.argv.includes('--update')) {\n return true;\n }\n return false;\n}\n\nexport interface JsonSnapshotOptions {\n update?: boolean;\n}\n\nfunction formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 4)}\\n`;\n}\n\n/**\n * Accessor for a JSON payload parsed from a text stream (stdout).\n *\n * Lazily parses the JSON on first use; parse errors are deferred until an\n * assertion is performed so that callers can still read the raw stream text\n * without triggering a throw.\n *\n * When a `transform` is configured on the spec runner, it is applied to the\n * raw stdout text BEFORE `JSON.parse`. This lets consumers strip ANSI codes,\n * scrub machine-specific paths, etc. before structural comparison.\n */\nexport class JsonAccessor {\n private readonly rawText: string;\n private readonly testDir: string;\n private readonly transform?: (text: string) => string;\n\n constructor(rawText: string, testDir: string, transform?: (text: string) => string) {\n this.rawText = rawText;\n this.testDir = testDir;\n this.transform = transform;\n }\n\n /** The parsed JSON value. Throws if the text is not valid JSON. */\n get value(): unknown {\n return this.parse();\n }\n\n /**\n * Assert the parsed JSON deep-equals the JSON in the given file on disk.\n *\n * Path is resolved relative to the test file directory unless absolute.\n * If `JTERRAZZ_TEST_UPDATE=1` (or vitest `-u`), the file is (re)written\n * with the pretty-printed parsed value.\n */\n toMatchFile(path: string, options: JsonSnapshotOptions = {}): void {\n const absPath = isAbsolute(path) ? path : resolve(this.testDir, path);\n const update = options.update ?? shouldUpdateSnapshots();\n const actual = this.parse();\n\n if (update) {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, formatJson(actual));\n return;\n }\n\n if (!existsSync(absPath)) {\n throw new Error(\n `JSON fixture \"${path}\" does not exist at ${absPath}.\\n` +\n `Run with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`,\n );\n }\n\n const expected = JSON.parse(readFileSync(absPath, 'utf8'));\n if (JSON.stringify(expected) !== JSON.stringify(actual)) {\n throw new Error(formatResponseDiff(path, expected, actual));\n }\n }\n\n /**\n * Assert the parsed JSON matches a convention-based fixture:\n * `<test-file-dir>/expected/json/<name>`.\n *\n * The extension is part of the name and must be included by the caller —\n * e.g. `toMatch('error.json')`, not `toMatch('error')`. Explicit extensions\n * are clearer at the call site and remove magic from the resolution.\n */\n toMatch(name: string, options: JsonSnapshotOptions = {}): void {\n const absPath = resolve(this.testDir, 'expected', 'json', name);\n this.toMatchFile(absPath, options);\n }\n\n private parse(): unknown {\n const source = this.transform ? this.transform(this.rawText) : this.rawText;\n try {\n return JSON.parse(source);\n } catch {\n const preview = source.slice(0, 200);\n throw new Error(`stdout is not valid JSON: ${preview}`);\n }\n }\n}\n","import type { DatabasePort } from '../ports/database.port.js';\nimport { formatTableDiff } from '../reporter.js';\n\n/** Assertion helper for verifying database table contents after a specification run. */\nexport class TableAssertion {\n private tableName: string;\n private db: DatabasePort;\n\n constructor(tableName: string, db: DatabasePort) {\n this.tableName = tableName;\n this.db = db;\n }\n\n /**\n * Assert that the table contains exactly the expected rows for the given columns.\n *\n * @example\n * await result.table(\"users\").toMatch({\n * columns: [\"name\", \"email\"],\n * rows: [[\"Alice\", \"alice@example.com\"]],\n * });\n */\n async toMatch(expected: { columns: string[]; rows: unknown[][] }): Promise<void> {\n const actual = await this.db.query(this.tableName, expected.columns);\n if (JSON.stringify(actual) !== JSON.stringify(expected.rows)) {\n throw new Error(\n formatTableDiff(this.tableName, expected.columns, expected.rows, actual),\n );\n }\n }\n\n /** Assert that the table has zero rows. */\n async toBeEmpty(): Promise<void> {\n const actual = await this.db.query(this.tableName, ['*']);\n if (actual.length !== 0) {\n throw new Error(\n `Expected table \"${this.tableName}\" to be empty, but it has ${actual.length} rows`,\n );\n }\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { SpecificationConfig } from '../builder.js';\nimport type { DatabasePort } from '../ports/database.port.js';\nimport { DirectoryAccessor } from './directory.js';\nimport { TableAssertion } from './table.js';\n\n/** Read-only handle to a single file produced by a spec action. */\nexport interface FileAccessor {\n /** The UTF-8 text content. Throws if the file does not exist. */\n readonly content: string;\n readonly exists: boolean;\n}\n\nexport interface BaseResultOptions {\n config: SpecificationConfig;\n testDir: string;\n workDir?: string;\n}\n\n/**\n * Base result - common accessors available after any action type.\n * Extended by HttpResult, CliResult, and used directly by JobResult.\n */\nexport class BaseResult {\n protected config: SpecificationConfig;\n protected testDir: string;\n protected workDir?: string;\n\n constructor(options: BaseResultOptions) {\n this.config = options.config;\n this.testDir = options.testDir;\n this.workDir = options.workDir;\n }\n\n /** Access a directory (relative to the working directory) for snapshot assertions. */\n directory(path: string = '.'): DirectoryAccessor {\n const baseDir = this.workDir ?? this.testDir;\n return new DirectoryAccessor(resolve(baseDir, path), this.testDir);\n }\n\n /** Access a single file (relative to the working directory) for content assertions. */\n file(path: string): FileAccessor {\n const baseDir = this.workDir ?? this.testDir;\n const resolvedPath = resolve(baseDir, path);\n const exists = existsSync(resolvedPath);\n return {\n get content(): string {\n if (!exists) {\n throw new Error(`File not found: ${path}`);\n }\n return readFileSync(resolvedPath, 'utf8');\n },\n exists,\n };\n }\n\n /** Access a database table for row-level assertions. */\n table(tableName: string, options?: { service?: string }): TableAssertion {\n const db = this.resolveDatabase(options?.service);\n if (!db) {\n throw new Error(\n options?.service\n ? `table(\"${tableName}\") requires database \"${options.service}\" but it was not found`\n : `table(\"${tableName}\") requires a database adapter`,\n );\n }\n return new TableAssertion(tableName, db);\n }\n\n private resolveDatabase(serviceName?: string): DatabasePort | undefined {\n if (serviceName && this.config.databases) {\n return this.config.databases.get(serviceName);\n }\n return this.config.database;\n }\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, isAbsolute, resolve } from 'node:path';\n\nimport { formatStdoutDiff } from '../reporter.js';\n\nfunction shouldUpdateSnapshots(): boolean {\n if (process.env.JTERRAZZ_TEST_UPDATE === '1') {\n return true;\n }\n if (process.env.UPDATE_SNAPSHOTS === '1') {\n return true;\n }\n if (process.argv.includes('-u') || process.argv.includes('--update')) {\n return true;\n }\n return false;\n}\n\nexport interface StreamSnapshotOptions {\n update?: boolean;\n}\n\n/**\n * Accessor for a captured text stream (stdout/stderr) with file-based\n * assertion support.\n *\n * Backed by a primitive string, exposed via {@link text}, but also provides\n * `toString()` / `valueOf()` for string coercion, so common patterns like\n * `String(result.stdout)` and template-literal interpolation still work.\n */\nexport class StreamAccessor {\n private readonly streamName: string;\n private readonly testDir: string;\n private readonly transform?: (text: string) => string;\n readonly text: string;\n\n constructor(\n text: string,\n streamName: string,\n testDir: string,\n transform?: (text: string) => string,\n ) {\n this.text = text;\n this.streamName = streamName;\n this.testDir = testDir;\n this.transform = transform;\n }\n\n /**\n * Assert the captured text matches the given file on disk.\n *\n * Path is resolved relative to the test file directory unless absolute.\n * If `JTERRAZZ_TEST_UPDATE=1` (or vitest `-u`), the file is (re)written\n * with the actual text.\n *\n * When a `transform` is configured on the spec runner, it is applied to\n * the actual text before comparison (and before writing in update mode).\n * The fixture file is treated as authoritative and is NOT transformed.\n */\n toMatchFile(path: string, options: StreamSnapshotOptions = {}): void {\n const absPath = isAbsolute(path) ? path : resolve(this.testDir, path);\n const update = options.update ?? shouldUpdateSnapshots();\n const actual = this.transform ? this.transform(this.text) : this.text;\n\n if (update) {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, actual);\n return;\n }\n\n if (!existsSync(absPath)) {\n throw new Error(\n `${this.streamName} fixture \"${path}\" does not exist at ${absPath}.\\n` +\n `Run with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`,\n );\n }\n\n const expected = readFileSync(absPath, 'utf8');\n if (expected !== actual) {\n throw new Error(formatStdoutDiff(path, expected, actual));\n }\n }\n\n /**\n * Assert the captured text matches a convention-based fixture:\n * `<test-file-dir>/expected/<stream>/<name>`.\n *\n * The extension is part of the name and must be included by the caller —\n * e.g. `toMatch('valid.txt')`, not `toMatch('valid')`. Explicit extensions\n * are clearer at the call site and remove magic from the resolution.\n */\n toMatch(name: string, options: StreamSnapshotOptions = {}): void {\n const absPath = resolve(this.testDir, 'expected', this.streamName, name);\n this.toMatchFile(absPath, options);\n }\n\n /**\n * Assert the captured text contains the given substring. The runner's\n * `transform` (if any) is applied before the check so callers can rely\n * on the same normalised view as {@link toMatch}.\n *\n * Throws with a tight diff-style error on miss so tests read uniformly\n * with the other accessor assertions (no reaching through `.text` into\n * `expect(...).toContain(...)`).\n */\n toContain(substring: string): void {\n const actual = this.transform ? this.transform(this.text) : this.text;\n if (actual.includes(substring)) {\n return;\n }\n throw new Error(\n `${this.streamName} does not contain expected substring.\\n` +\n ` expected to contain: ${JSON.stringify(substring)}\\n` +\n ` actual: ${JSON.stringify(actual.length > 500 ? `${actual.slice(0, 500)}…` : actual)}`,\n );\n }\n\n toString(): string {\n return this.text;\n }\n\n valueOf(): string {\n return this.text;\n }\n}\n","import { execSync } from 'node:child_process';\n\nimport { JsonAccessor } from '../../result/json.js';\nimport type { FileAccessor } from '../../result/result.js';\nimport { StreamAccessor } from '../../result/stream.js';\nimport type { CommandResult } from './command.port.js';\nimport { CliResult } from './result.js';\n\nconst EXEC_TIMEOUT = 10_000;\n\nfunction readInspectState(inspect: any): { running: boolean; status: string } {\n const state = inspect?.State ?? inspect?.state;\n if (!state) {\n return { running: false, status: 'unknown' };\n }\n return {\n running: Boolean(state.Running ?? state.running ?? false),\n status: String(state.Status ?? state.status ?? 'unknown'),\n };\n}\n\n/**\n * Assertion accessor for a single Docker container captured by the docker()\n * spec mode. Mirrors the shape of {@link CliResult} so tests use the same\n * vocabulary (`stdout.toContain`, `file(...).content`, etc.) regardless of\n * where output came from.\n *\n * Sync state (`exists`, `running`, `status`) is derived from the `docker\n * inspect` payload captured at result-construction time. Logs are fetched\n * lazily on first access to `stdout`/`stderr`.\n */\nexport class ContainerAccessor {\n private cachedLogs: null | string = null;\n private readonly containerId: null | string;\n readonly exists: boolean;\n private readonly inspectData: unknown;\n readonly running: boolean;\n readonly status: string;\n private readonly testDir: string;\n private readonly transform?: (text: string) => string;\n\n /**\n * Underlying Docker container ID, or `null` if no container was captured\n * for this name. Useful when a follow-up CLI command needs to reference\n * the container by id (e.g. `spwn world inspect <id>`). Prefer the other\n * accessors for common reads — pulling the raw id is the escape hatch.\n */\n get id(): null | string {\n return this.containerId;\n }\n\n constructor(\n containerId: null | string,\n inspectData: unknown,\n testDir: string,\n transform?: (text: string) => string,\n ) {\n this.containerId = containerId;\n this.inspectData = inspectData;\n this.testDir = testDir;\n this.transform = transform;\n this.exists = containerId !== null;\n if (this.exists) {\n const state = readInspectState(inspectData);\n this.running = state.running;\n this.status = state.status;\n } else {\n this.running = false;\n this.status = 'absent';\n }\n }\n\n /**\n * Raw `docker inspect` object for this container. Throws if the container\n * was not captured (i.e. `.exists === false`).\n */\n get inspect(): JsonAccessor {\n this.requireExists('inspect');\n return new JsonAccessor(JSON.stringify(this.inspectData), this.testDir, this.transform);\n }\n\n /** Captured logs (stdout+stderr combined for v1). */\n get stdout(): StreamAccessor {\n this.requireExists('stdout');\n return new StreamAccessor(this.loadLogs(), 'stdout', this.testDir, this.transform);\n }\n\n /** Captured logs (stdout+stderr combined for v1). */\n get stderr(): StreamAccessor {\n this.requireExists('stderr');\n return new StreamAccessor(this.loadLogs(), 'stderr', this.testDir, this.transform);\n }\n\n /**\n * Read a file from inside the container via `docker exec cat`. The\n * returned object satisfies the same {@link FileAccessor} shape used by\n * the host filesystem accessor, so tests do not need to learn a new API.\n */\n file(path: string): FileAccessor {\n this.requireExists('file');\n const id = this.containerId!;\n const exists = this.containerExec(id, ['test', '-e', path]).exitCode === 0;\n return {\n get content(): string {\n if (!exists) {\n throw new Error(`File not found in container: ${path}`);\n }\n const result = execSync(`docker exec ${id} cat ${JSON.stringify(path)}`, {\n encoding: 'utf8',\n timeout: EXEC_TIMEOUT,\n });\n return result;\n },\n exists,\n };\n }\n\n /**\n * Run a shell command inside the container and get back the same\n * {@link CliResult} used for host-side executions.\n */\n async exec(cmd: string): Promise<CliResult> {\n this.requireExists('exec');\n const commandResult = this.containerExec(this.containerId!, ['sh', '-c', cmd]);\n return new CliResult({\n commandResult,\n config: {},\n testDir: this.testDir,\n transform: this.transform,\n workDir: this.testDir,\n });\n }\n\n private requireExists(member: string): void {\n if (!this.exists) {\n throw new Error(\n `ContainerAccessor.${member}: container does not exist (check .exists first)`,\n );\n }\n }\n\n private loadLogs(): string {\n if (this.cachedLogs !== null) {\n return this.cachedLogs;\n }\n try {\n const out = execSync(`docker logs ${this.containerId}`, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: EXEC_TIMEOUT,\n });\n this.cachedLogs = out;\n } catch (error: any) {\n // Fall back to whatever the CLI managed to write before exiting\n // (docker logs may fail if the container disappeared mid-run).\n this.cachedLogs =\n typeof error?.stdout === 'string' ? error.stdout : String(error?.stderr ?? '');\n }\n return this.cachedLogs ?? '';\n }\n\n private containerExec(id: string, argv: string[]): CommandResult {\n // Piping stdio keeps a non-zero exit inside a CommandResult instead of throwing out.\n try {\n const quoted = argv.map((a) => JSON.stringify(a)).join(' ');\n const stdout = execSync(`docker exec ${id} ${quoted}`, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: EXEC_TIMEOUT,\n });\n return { exitCode: 0, stderr: '', stdout };\n } catch (error: any) {\n return {\n exitCode: typeof error?.status === 'number' ? error.status : 1,\n stderr:\n typeof error?.stderr === 'string' ? error.stderr : String(error?.message ?? ''),\n stdout: typeof error?.stdout === 'string' ? error.stdout : '',\n };\n }\n }\n}\n","import { execSync } from 'node:child_process';\n\n/**\n * Thin synchronous wrappers around the `docker` CLI used by the docker() spec\n * mode. Sync is deliberate — the calls are fast, run only on the host, and\n * keep the dispose path straightforward (`Symbol.asyncDispose` can still be\n * async without needing these to be).\n */\n\nconst DEFAULT_TIMEOUT = 10_000;\n\n/** Return all container IDs (running or stopped) that carry `key=value`. */\nexport function findContainersByLabel(key: string, value: string): string[] {\n try {\n const raw = execSync(`docker ps -aq -f label=${key}=${value}`, {\n encoding: 'utf8',\n timeout: DEFAULT_TIMEOUT,\n });\n return raw\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n } catch {\n return [];\n }\n}\n\n/** Return the raw `docker inspect` payload (object, not array) for a container. */\nexport function inspectContainer(id: string): unknown {\n const raw = execSync(`docker inspect ${id}`, {\n encoding: 'utf8',\n timeout: DEFAULT_TIMEOUT,\n });\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed[0] : parsed;\n}\n\n/** Force-remove the given container IDs in a single call. Errors are swallowed. */\nexport function removeContainers(ids: string[]): void {\n if (ids.length === 0) {\n return;\n }\n try {\n execSync(`docker rm -f ${ids.join(' ')}`, {\n encoding: 'utf8',\n stdio: 'pipe',\n timeout: DEFAULT_TIMEOUT,\n });\n } catch {\n // Ignore — containers may already be gone.\n }\n}\n","import type { DockerSpecConfig, SpecificationConfig } from '../../builder.js';\nimport { FilesystemAccessor } from '../../result/filesystem.js';\nimport { grep as grepUtil } from '../../result/grep.js';\nimport { JsonAccessor } from '../../result/json.js';\nimport { BaseResult } from '../../result/result.js';\nimport { StreamAccessor } from '../../result/stream.js';\nimport type { CommandResult } from './command.port.js';\nimport { ContainerAccessor } from './container-accessor.js';\nimport { findContainersByLabel, inspectContainer, removeContainers } from './docker-lookup.js';\n\ninterface CapturedContainer {\n id: string;\n inspect: unknown;\n}\n\n/**\n * Result from a CLI action (.exec(), .spawn()).\n *\n * When the runner was configured with a `docker` option, the result also\n * exposes container accessors and participates in `Symbol.asyncDispose`\n * cleanup. The Docker shell-outs are **lazy**: a test that never calls\n * `.container(...)` never queries the Docker daemon, so CLI-only tests\n * pay zero Docker cost even when the runner is Docker-aware.\n *\n * Dispose always runs one final label-filtered `docker ps` to catch\n * containers that were spawned during `.exec` but never asserted on —\n * tests still get leak-free cleanup even if they forget to reach into\n * a container.\n */\nexport class CliResult extends BaseResult {\n private readonly commandResult: CommandResult;\n private containersCache: Map<string, CapturedContainer> | null = null;\n private readonly dockerConfig?: DockerSpecConfig;\n private readonly testRunId?: string;\n private readonly transform?: (text: string) => string;\n\n constructor(options: {\n commandResult: CommandResult;\n config: SpecificationConfig;\n dockerConfig?: DockerSpecConfig;\n testDir: string;\n testRunId?: string;\n transform?: (text: string) => string;\n workDir: string;\n }) {\n super(options);\n this.commandResult = options.commandResult;\n this.dockerConfig = options.dockerConfig;\n this.testRunId = options.testRunId;\n this.transform = options.transform;\n }\n\n /** The process exit code. */\n get exitCode(): number {\n return this.commandResult.exitCode;\n }\n\n /** Accessor for the captured standard output with file-based assertions. */\n get stdout(): StreamAccessor {\n return new StreamAccessor(\n this.commandResult.stdout,\n 'stdout',\n this.testDir,\n this.transform,\n );\n }\n\n /** Accessor for the captured standard error with file-based assertions. */\n get stderr(): StreamAccessor {\n return new StreamAccessor(\n this.commandResult.stderr,\n 'stderr',\n this.testDir,\n this.transform,\n );\n }\n\n /** Accessor for parsing stdout as JSON and asserting against JSON fixtures. */\n get json(): JsonAccessor {\n return new JsonAccessor(this.commandResult.stdout, this.testDir, this.transform);\n }\n\n /** Accessor for the temporary working directory the command ran in. */\n get filesystem(): FilesystemAccessor {\n if (!this.workDir) {\n throw new Error('CliResult.filesystem requires a working directory');\n }\n return new FilesystemAccessor(this.workDir, this.testDir);\n }\n\n /**\n * Look up a container the CLI spawned during this run by the value of\n * its name-label (as declared in `SpecOptions.docker.nameLabel`).\n * First access triggers a one-shot `docker ps` + `docker inspect`\n * query and caches the result for the rest of the result's lifetime.\n * Tests that don't call this never touch Docker.\n *\n * Returns an accessor with `.exists === false` when no container\n * carries that name — callers can still assert absence without a try.\n */\n container(name: string): ContainerAccessor {\n this.ensureDockerAware('container');\n this.loadContainers();\n const captured = this.containersCache!.get(name);\n if (!captured) {\n return new ContainerAccessor(null, null, this.testDir, this.transform);\n }\n return new ContainerAccessor(captured.id, captured.inspect, this.testDir, this.transform);\n }\n\n /** All captured container IDs. Triggers the same lazy query as `container()`. */\n get containerIds(): string[] {\n this.ensureDockerAware('containerIds');\n this.loadContainers();\n return [...this.containersCache!.values()].map((c) => c.id);\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n if (!this.dockerConfig || !this.testRunId) {\n return;\n }\n // If the test touched `.container(...)` we already have the id set.\n // Otherwise re-query once to catch containers spawned during `.exec`\n // That the test never asserted on — still clean them up so tests\n // Running in parallel never collide.\n const ids =\n this.containersCache !== null\n ? [...this.containersCache.values()].map((c) => c.id)\n : findContainersByLabel(this.dockerConfig.testRunLabel, this.testRunId);\n removeContainers(ids);\n }\n\n /**\n * Extract text blocks from stdout that contain a pattern.\n *\n * @example\n * expect(result.grep('error.ts')).toContain('no-unused-vars');\n */\n grep(pattern: string): string {\n return grepUtil(this.commandResult.stdout, pattern);\n }\n\n private ensureDockerAware(member: string): void {\n if (!this.dockerConfig || !this.testRunId) {\n throw new Error(\n `CliResult.${member}: runner was not configured with a docker option. ` +\n `Pass \\`docker: { envVar, nameLabel, testRunLabel }\\` in SpecOptions.`,\n );\n }\n }\n\n private loadContainers(): void {\n if (this.containersCache !== null) {\n return;\n }\n const ids = findContainersByLabel(this.dockerConfig!.testRunLabel, this.testRunId!);\n const map = new Map<string, CapturedContainer>();\n for (const id of ids) {\n let inspect: unknown;\n try {\n inspect = inspectContainer(id);\n } catch {\n continue;\n }\n const labels =\n (inspect as any)?.Config?.Labels ?? (inspect as any)?.config?.labels ?? {};\n const nameValue = labels[this.dockerConfig!.nameLabel];\n const key = typeof nameValue === 'string' && nameValue.length > 0 ? nameValue : id;\n map.set(key, { id, inspect });\n }\n this.containersCache = map;\n }\n}\n","import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { formatResponseDiff } from '../reporter.js';\n\n/** Accessor for an HTTP response body with file-based assertion support. */\nexport class ResponseAccessor {\n readonly body: unknown;\n private testDir: string;\n\n constructor(body: unknown, testDir: string) {\n this.body = body;\n this.testDir = testDir;\n }\n\n /**\n * Assert that the response body matches the JSON in `responses/{file}`.\n *\n * @example\n * result.response.toMatchFile(\"expected-items.json\");\n */\n toMatchFile(file: string): void {\n const expected = JSON.parse(readFileSync(resolve(this.testDir, 'responses', file), 'utf8'));\n if (JSON.stringify(this.body) !== JSON.stringify(expected)) {\n throw new Error(formatResponseDiff(file, expected, this.body));\n }\n }\n}\n","import type { SpecificationConfig } from '../../builder.js';\nimport { ResponseAccessor } from '../../result/response.js';\nimport { BaseResult } from '../../result/result.js';\nimport type { ServerResponse } from './server.port.js';\n\n/** Result from an HTTP action (.get(), .post(), .put(), .delete()). */\nexport class HttpResult extends BaseResult {\n private responseData: ServerResponse;\n\n constructor(options: {\n config: SpecificationConfig;\n response: ServerResponse;\n testDir: string;\n }) {\n super(options);\n this.responseData = options.response;\n }\n\n /** The HTTP response status code. */\n get status(): number {\n return this.responseData.status;\n }\n\n /** Access the HTTP response body for assertions. */\n get response(): ResponseAccessor {\n return new ResponseAccessor(this.responseData.body, this.testDir);\n }\n}\n","import { cpSync, existsSync, mkdtempSync, readFileSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { resolve } from 'node:path';\n\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { InterceptEntry, InterceptResponse, InterceptTrigger } from './intercept/types.js';\nimport type {\n CommandEnv,\n CommandPort,\n CommandResult,\n SpawnOptions,\n} from './modes/cli/command.port.js';\nimport { CliResult } from './modes/cli/result.js';\nimport { HttpResult } from './modes/http/result.js';\nimport type { ServerPort } from './modes/http/server.port.js';\nimport { BaseResult } from './result/result.js';\n\n// ── Types ──\n\n/** A named job that can be triggered via .job(). */\nexport interface JobHandle {\n name: string;\n execute: () => Promise<void>;\n}\n\n/**\n * Context passed to a {@link SeedHandler} when a non-SQL seed is dispatched.\n */\nexport interface SeedHandlerContext {\n /** Absolute path of the temporary working directory for the current spec. */\n cwd: string;\n /** Absolute path of the test file directory (where `seeds/` lives). */\n testDir: string;\n}\n\n/**\n * Handler invoked for a non-SQL seed entry. The first path segment of the\n * seed file selects the handler; the handler is given the absolute path of\n * the source fragment (file or directory) and the working directory to mutate.\n */\nexport type SeedHandler = (ctx: SeedHandlerContext, fragmentPath: string) => Promise<void> | void;\n\n/**\n * Configuration for the docker() spec mode. When set on\n * {@link SpecificationConfig}, the CLI runner generates a test-run id, injects\n * it into the child env under `envVar`, then queries Docker for every\n * container carrying `testRunLabel=<id>` after the command exits.\n */\nexport interface DockerSpecConfig {\n envVar: string;\n nameLabel: string;\n testRunLabel: string;\n}\n\n/** Adapter configuration passed to the specification runner at setup time. */\nexport interface SpecificationConfig {\n command?: CommandPort;\n database?: DatabasePort;\n databases?: Map<string, DatabasePort>;\n dockerConfig?: DockerSpecConfig;\n /**\n * Unique id shared by every `.run()` from this runner instance.\n * Stable for the runner's lifetime so multi-step tests (spawn in\n * one run, inspect in another) see the same container scope. The\n * public `createSpecificationRunner` auto-populates this when\n * `dockerConfig` is present — callers should leave it unset.\n */\n dockerTestRunId?: string;\n fixturesRoot?: string;\n jobs?: JobHandle[];\n /**\n * Pluggable seed handlers for CLI-mode tests. Keys are leading path\n * segments (e.g. `\"spwn.yaml/\"`, `\"agent/\"`). When `.seed(relPath)` is\n * called with a path whose first segment matches, the handler is invoked\n * with the full absolute path of the seed fragment under\n * `<test-file-dir>/seeds/<relPath>`.\n */\n seedHandlers?: Record<string, SeedHandler>;\n server?: ServerPort;\n /**\n * Optional normaliser applied to CLI stdout/stderr before every\n * .toMatch / .toMatchFile comparison. Does not mutate the raw\n * `.text` accessor.\n */\n transform?: (text: string) => string;\n}\n\n/** A SQL seed file to execute before the action, optionally targeting a named database service. */\nexport interface SeedEntry {\n file: string;\n service?: string;\n}\n\n/** A fixture file or directory to copy into the working directory before execution. */\nexport interface FixtureEntry {\n file: string;\n}\n\n/** An MSW mock definition file to register before the action. */\nexport interface MockEntry {\n file: string;\n}\n\n/** An HTTP request to perform against the server adapter. */\nexport interface RequestEntry {\n bodyFile?: string;\n headers?: Record<string, string>;\n method: string;\n path: string;\n}\n\n// ── Builder (before .run()) ──\n\n/**\n * Fluent builder for declaring a single test specification.\n *\n * Chain setup methods ({@link seed}, {@link fixture}, {@link env}), an action\n * ({@link get}, {@link post}, {@link exec}), then call {@link run} to execute\n * and receive a {@link SpecificationResult} for assertions.\n */\nexport class SpecificationBuilder {\n private commandArgs: null | string | string[] = null;\n private commandEnv: CommandEnv = {};\n private config: SpecificationConfig;\n private fixtures: FixtureEntry[] = [];\n private intercepts: InterceptEntry[] = [];\n private jobName: null | string = null;\n private label: string;\n private mocks: MockEntry[] = [];\n private projectName: null | string = null;\n private request: null | RequestEntry = null;\n private requestHeaders: Record<string, string> = {};\n private seeds: SeedEntry[] = [];\n private spawnConfig: null | { args: string; options: SpawnOptions } = null;\n private testDir: string;\n\n constructor(config: SpecificationConfig, testDir: string, label: string) {\n this.config = config;\n this.testDir = testDir;\n this.label = label;\n }\n\n // ── Setup ──\n\n /**\n * Queue a SQL seed file to run before the action.\n *\n * @example\n * spec(\"creates user\").seed(\"users.sql\").exec(\"list-users\").run();\n */\n seed(file: string, options?: { service?: string }): this {\n this.seeds.push({ file, service: options?.service });\n return this;\n }\n\n /** Copy a file or directory from `fixtures/` into the working directory before execution. */\n fixture(file: string): this {\n this.fixtures.push({ file });\n return this;\n }\n\n /** Copy an entire project fixture from `fixturesRoot` into the working directory. */\n project(name: string): this {\n this.projectName = name;\n return this;\n }\n\n /** Register an MSW mock definition file from `mock/` to intercept HTTP calls. */\n mock(file: string): this {\n this.mocks.push({ file });\n return this;\n }\n\n /**\n * Set environment variables for the CLI process. Merged on top of process.env.\n * Use `null` to unset a variable. Multiple calls merge.\n *\n * The token `$WORKDIR` (in any value) is replaced with the actual working\n * directory at run-time — useful for tests that need a fully isolated `HOME`.\n *\n * @example\n * spec(\"...\").env({ HOME: \"$WORKDIR\", TZ: \"UTC\" }).exec(\"status\").run();\n */\n env(env: CommandEnv): this {\n this.commandEnv = { ...this.commandEnv, ...env };\n return this;\n }\n\n /**\n * Set HTTP headers for the request. Multiple calls merge.\n *\n * @example\n * spec(\"french\").headers({ 'Accept-Language': 'fr' }).get(\"/articles\").run();\n */\n headers(headers: Record<string, string>): this {\n this.requestHeaders = { ...this.requestHeaders, ...headers };\n return this;\n }\n\n /**\n * Intercept an outgoing HTTP request and return a controlled response.\n * Uses MSW under the hood. Intercepts are queued — multiple calls with the\n * same trigger fire sequentially (first match consumed first).\n *\n * @param trigger - What to match (use openai.agent(), http.get(), etc.)\n * @param response - What to return: an InterceptResponse object, or a filename\n * from `intercepts/` (JSON loaded and auto-wrapped by the trigger).\n *\n * @example\n * // With inline response\n * .intercept(openai.agent({...}), openai.reply({ categories: ['TECH'] }))\n *\n * // With JSON fixture file (loaded from intercepts/ directory)\n * .intercept(openai.agent({...}), 'ingest-tech.json')\n * .intercept(http.get(url), 'world-news-tech.json')\n */\n intercept(trigger: InterceptTrigger, response: InterceptResponse | string): this {\n if (typeof response === 'string') {\n // File path format: 'adapter/filename.json'\n const slashIndex = response.indexOf('/');\n if (slashIndex === -1) {\n throw new Error(\n `.intercept(): file path must be 'adapter/filename.json' (e.g. 'openai/ingest-tech.json'), got '${response}'`,\n );\n }\n\n const adapterName = response.slice(0, slashIndex);\n if (adapterName !== trigger.adapter) {\n throw new Error(\n `.intercept(): adapter mismatch - trigger uses '${trigger.adapter}' but file path starts with '${adapterName}/'`,\n );\n }\n\n const filePath = resolve(this.testDir, 'intercepts', response);\n const data = JSON.parse(readFileSync(filePath, 'utf8'));\n const resolved = trigger.wrap(data);\n this.intercepts.push({ trigger, response: resolved });\n } else {\n this.intercepts.push({ trigger, response });\n }\n return this;\n }\n\n // ── HTTP actions ──\n\n /**\n * Send a GET request to the server adapter.\n *\n * @example\n * spec(\"list items\").get(\"/api/items\").run();\n */\n get(path: string): this {\n this.request = { method: 'GET', path };\n return this;\n }\n\n /**\n * Send a POST request to the server adapter.\n *\n * @param bodyFile - Optional JSON file from `requests/` to use as the request body.\n * @example\n * spec(\"create item\").post(\"/api/items\", \"new-item.json\").run();\n */\n post(path: string, bodyFile?: string): this {\n this.request = { bodyFile, method: 'POST', path };\n return this;\n }\n\n /** Send a PUT request to the server adapter. */\n put(path: string, bodyFile?: string): this {\n this.request = { bodyFile, method: 'PUT', path };\n return this;\n }\n\n /** Send a DELETE request to the server adapter. */\n delete(path: string): this {\n this.request = { method: 'DELETE', path };\n return this;\n }\n\n // ── CLI actions ──\n\n /**\n * Execute a CLI command (or a sequence of commands) in an isolated working directory.\n * When an array is passed, commands run sequentially and stop on the first non-zero exit code.\n *\n * @example\n * spec(\"init project\").exec(\"init --name demo\").run();\n * spec(\"multi-step\").exec([\"init\", \"build\"]).run();\n */\n exec(args: string | string[]): this {\n this.commandArgs = args;\n return this;\n }\n\n /** Spawn a long-running CLI process with custom spawn options (e.g. timeout, signal). */\n spawn(args: string, options: SpawnOptions): this {\n this.spawnConfig = { args, options };\n return this;\n }\n\n // ── Job actions ──\n\n /**\n * Execute a named job registered via the app() factory.\n *\n * @param name - The job name to trigger (must match a registered JobHandle.name).\n *\n * @example\n * spec('pipeline').intercept(openai.chat(), openai.response({...})).job('report-refresh').run();\n */\n job(name: string): this {\n this.jobName = name;\n return this;\n }\n\n // ── Run ──\n\n /**\n * Execute the specification: run seeds, copy fixtures, then perform the\n * configured action (HTTP or CLI).\n *\n * @returns The result object used for assertions.\n * @example\n * const result = await spec(\"test\").exec(\"status\").run();\n * expect(result.exitCode).toBe(0);\n */\n async run(): Promise<BaseResult | CliResult | HttpResult> {\n const hasHttpAction = this.request !== null;\n const hasCliAction = this.commandArgs !== null || this.spawnConfig !== null;\n const hasJobAction = this.jobName !== null;\n\n const actionCount = [hasHttpAction, hasCliAction, hasJobAction].filter(Boolean).length;\n\n if (actionCount === 0) {\n throw new Error(\n `Specification \"${this.label}\": no action defined. Call .get(), .post(), .exec(), .job(), etc. before .run()`,\n );\n }\n\n if (actionCount > 1) {\n throw new Error(\n `Specification \"${this.label}\": cannot mix action types (.get/.post, .exec/.spawn, .job)`,\n );\n }\n\n let workDir: null | string = null;\n if (hasCliAction) {\n workDir = this.prepareWorkDir();\n }\n\n // Reset all databases\n if (this.config.databases) {\n for (const db of this.config.databases.values()) {\n await db.reset();\n }\n } else if (this.config.database) {\n await this.config.database.reset();\n }\n\n // Execute seeds\n for (const entry of this.seeds) {\n // Pluggable seed handlers (CLI mode): dispatch based on leading path segment.\n const handler = this.resolveSeedHandler(entry.file);\n if (handler) {\n if (!workDir) {\n throw new Error(\n `seed(\"${entry.file}\"): pluggable seed handlers require a CLI working directory`,\n );\n }\n const fragmentPath = resolve(this.testDir, 'seeds', entry.file);\n await handler({ cwd: workDir, testDir: this.testDir }, fragmentPath);\n continue;\n }\n\n // Default: SQL seed against a database adapter.\n let db: DatabasePort | undefined;\n if (entry.service && this.config.databases) {\n db = this.config.databases.get(entry.service);\n if (!db) {\n throw new Error(\n `seed() targets database \"${entry.service}\" but it was not found. Available: ${[...this.config.databases.keys()].join(', ')}`,\n );\n }\n } else {\n db = this.config.database;\n }\n\n if (!db) {\n throw new Error('seed() requires a database adapter');\n }\n\n const sql = readFileSync(resolve(this.testDir, 'seeds', entry.file), 'utf8');\n await db.seed(sql);\n }\n\n // Copy fixture files into working directory\n if (this.fixtures.length > 0 && workDir) {\n for (const entry of this.fixtures) {\n const src = resolve(this.testDir, 'fixtures', entry.file);\n const dest = resolve(workDir, entry.file);\n cpSync(src, dest, { recursive: true });\n }\n }\n\n // Register HTTP intercepts via MSW\n let cleanupIntercepts: (() => void) | null = null;\n if (this.intercepts.length > 0) {\n const { registerIntercepts } = await import('./intercept/intercept.js');\n cleanupIntercepts = await registerIntercepts(this.intercepts);\n }\n\n // Execute action\n try {\n if (hasHttpAction) {\n return await this.runHttpAction();\n }\n if (hasJobAction) {\n return await this.runJobAction();\n }\n return await this.runCliAction(workDir!);\n } finally {\n if (cleanupIntercepts) {\n cleanupIntercepts();\n }\n }\n }\n\n // ── Private ──\n\n private resolveSeedHandler(file: string): SeedHandler | undefined {\n const handlers = this.config.seedHandlers;\n if (!handlers) {\n return undefined;\n }\n // Prefer the longest matching prefix, so 'agent/neo/' can override 'agent/'.\n const normalized = file.replaceAll('\\\\', '/');\n let bestKey: string | undefined;\n for (const key of Object.keys(handlers)) {\n const prefix = key.endsWith('/') ? key : `${key}/`;\n if (normalized === key || normalized.startsWith(prefix)) {\n if (!bestKey || key.length > bestKey.length) {\n bestKey = key;\n }\n }\n }\n return bestKey ? handlers[bestKey] : undefined;\n }\n\n private resolveEnv(workDir: string): CommandEnv | undefined {\n const keys = Object.keys(this.commandEnv);\n if (keys.length === 0) {\n return undefined;\n }\n\n const resolved: CommandEnv = {};\n for (const key of keys) {\n const value = this.commandEnv[key];\n resolved[key] =\n typeof value === 'string' ? value.replace(/\\$WORKDIR/g, workDir) : value;\n }\n return resolved;\n }\n\n private prepareWorkDir(): string {\n // Every CLI spec runs in a fresh, empty temp directory unless a project\n // Fixture is explicitly copied in via .project() (or files via .fixture()).\n // This guarantees isolation — the runner never writes into fixturesRoot.\n const tempDir = mkdtempSync(resolve(tmpdir(), 'spec-cli-'));\n\n if (this.projectName && this.config.fixturesRoot) {\n const projectDir = resolve(this.config.fixturesRoot, this.projectName);\n if (!existsSync(projectDir)) {\n throw new Error(\n `project(\"${this.projectName}\"): fixture project not found at ${projectDir}`,\n );\n }\n cpSync(projectDir, tempDir, { recursive: true });\n }\n\n return tempDir;\n }\n\n private async runHttpAction(): Promise<HttpResult> {\n if (!this.config.server) {\n throw new Error('HTTP actions require a server adapter (use integration() or e2e())');\n }\n\n let body: unknown;\n if (this.request!.bodyFile) {\n body = JSON.parse(\n readFileSync(resolve(this.testDir, 'requests', this.request!.bodyFile), 'utf8'),\n );\n }\n\n const headers =\n Object.keys(this.requestHeaders).length > 0 ? this.requestHeaders : undefined;\n const response = await this.config.server.request(\n this.request!.method,\n this.request!.path,\n body,\n headers,\n );\n\n return new HttpResult({\n config: this.config,\n response,\n testDir: this.testDir,\n });\n }\n\n private async runJobAction(): Promise<BaseResult> {\n if (!this.config.jobs?.length) {\n throw new Error(\n 'Job actions require jobs registered via app(() => ({ server, jobs }))',\n );\n }\n\n const job = this.config.jobs.find((j) => j.name === this.jobName);\n if (!job) {\n const available = this.config.jobs.map((j) => j.name).join(', ');\n throw new Error(`job(\"${this.jobName}\"): not found. Available: ${available}`);\n }\n\n await job.execute();\n\n return new BaseResult({\n config: this.config,\n testDir: this.testDir,\n });\n }\n\n private async runCliAction(workDir: string): Promise<CliResult> {\n if (!this.config.command) {\n throw new Error('CLI actions require a command adapter (use cli())');\n }\n\n const dockerConfig = this.config.dockerConfig;\n // The test-run id is bound to the SpecificationConfig (i.e. to\n // The runner), not to each .run() call. This means every spec\n // From the same runner sees the same isolation scope — tests\n // That spawn a world in one .run() and inspect/destroy it in\n // A follow-up .run() see their own container, not a ghost.\n // Vitest's fileParallelism gives each file its own process /\n // Module load, so different test files get different ids\n // Automatically (one createSpecificationRunner call each).\n const testRunId = this.config.dockerTestRunId;\n\n // Merge dockerConfig env var in first, then user env (which wins).\n let env = this.resolveEnv(workDir);\n if (dockerConfig && testRunId) {\n env = { [dockerConfig.envVar]: testRunId, ...env };\n }\n let commandResult: CommandResult;\n\n if (this.spawnConfig) {\n commandResult = await this.config.command.spawn(\n this.spawnConfig.args,\n workDir,\n this.spawnConfig.options,\n env,\n );\n } else if (Array.isArray(this.commandArgs)) {\n commandResult = { exitCode: 0, stderr: '', stdout: '' };\n for (const args of this.commandArgs) {\n commandResult = await this.config.command.exec(args, workDir, env);\n if (commandResult.exitCode !== 0) {\n break;\n }\n }\n } else {\n commandResult = await this.config.command.exec(this.commandArgs!, workDir, env);\n }\n\n return new CliResult({\n commandResult,\n config: this.config,\n dockerConfig: dockerConfig ?? undefined,\n testDir: this.testDir,\n testRunId: testRunId ?? undefined,\n transform: this.config.transform,\n workDir,\n });\n }\n}\n\n// ── Caller detection ──\n\nfunction getCallerDir(): string {\n const stack = new Error('caller detection').stack;\n if (!stack) {\n throw new Error('Cannot detect caller directory: no stack trace');\n }\n\n const lines = stack.split('\\n');\n for (const line of lines) {\n const match = line.match(/at\\s+(?:.*?\\()?(?:file:\\/\\/)?([^:)]+):\\d+:\\d+/);\n if (!match) {\n continue;\n }\n\n const filePath = match[1];\n\n if (filePath.includes('node_modules')) {\n continue;\n }\n if (filePath.includes('/src/builder/') || filePath.includes('/src/spec/')) {\n continue;\n }\n\n return resolve(filePath, '..');\n }\n\n throw new Error('Cannot detect caller directory from stack trace');\n}\n\n// ── Factory functions ──\n\n/** Factory function returned by `cli()`, `integration()`, or `e2e()` that starts a new spec. */\nexport type SpecificationRunner = (label: string) => SpecificationBuilder;\n\n/**\n * Create a {@link SpecificationRunner} bound to the given adapter configuration.\n * The test file directory is auto-detected from the call stack.\n */\nexport function createSpecificationRunner(config: SpecificationConfig): SpecificationRunner {\n const resolved: SpecificationConfig =\n config.dockerConfig && !config.dockerTestRunId\n ? {\n ...config,\n dockerTestRunId: `t-${Math.random().toString(36).slice(2, 10)}-${Date.now().toString(36)}`,\n }\n : config;\n return (label: string) => {\n const testDir = getCallerDir();\n return new SpecificationBuilder(resolved, testDir, label);\n };\n}\n","import { spawn, spawnSync } from 'node:child_process';\n\nimport type { CommandEnv, CommandPort, CommandResult, SpawnOptions } from '../command.port.js';\n\n/**\n * Build a child-process env from the parent env plus user overrides.\n * `null` overrides delete keys (e.g. `INIT_CWD: null`).\n */\nfunction buildEnv(extra?: CommandEnv): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env, INIT_CWD: undefined };\n if (extra) {\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) {\n delete env[key];\n } else {\n env[key] = value;\n }\n }\n }\n return env;\n}\n\n/**\n * Executes CLI commands via Node.js child_process.\n * Uses `spawnSync` for one-shot commands and `spawn` for long-running processes.\n * Used by the command() specification runner.\n *\n * `spawnSync` is used (over the simpler `execSync`) so stdout AND stderr are\n * captured regardless of exit code. Many CLIs follow the Unix convention of\n * writing status banners to stderr on success — `execSync` would silently\n * discard them, leaving snapshot tests with no output to assert on.\n */\nexport class ExecAdapter implements CommandPort {\n private command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n\n async exec(args: string, cwd: string, extraEnv?: CommandEnv): Promise<CommandResult> {\n const env = buildEnv(extraEnv);\n const result = spawnSync(`${this.command} ${args}`, [], {\n cwd,\n encoding: 'utf8',\n env,\n shell: true,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return {\n exitCode: result.status ?? 1,\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n };\n }\n\n async spawn(\n args: string,\n cwd: string,\n options: SpawnOptions,\n extraEnv?: CommandEnv,\n ): Promise<CommandResult> {\n const env = buildEnv(extraEnv);\n\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let resolved = false;\n\n const child = spawn(this.command, args.split(/\\s+/).filter(Boolean), {\n cwd,\n env,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n const finish = (exitCode: number) => {\n if (resolved) {\n return;\n }\n resolved = true;\n child.kill('SIGTERM');\n resolve({ exitCode, stdout, stderr });\n };\n\n let patternMatched = false;\n\n const checkPattern = () => {\n if (\n !patternMatched &&\n (stdout.includes(options.waitFor) || stderr.includes(options.waitFor))\n ) {\n patternMatched = true;\n finish(0);\n }\n };\n\n child.stdout?.on('data', (data: Buffer) => {\n stdout += data.toString();\n checkPattern();\n });\n\n child.stderr?.on('data', (data: Buffer) => {\n stderr += data.toString();\n checkPattern();\n });\n\n // Process exited before pattern matched\n child.on('exit', (code) => {\n if (!patternMatched) {\n finish(code === 0 ? 1 : (code ?? 1));\n }\n });\n\n setTimeout(() => finish(124), options.timeout);\n });\n }\n}\n","import type { ServerPort, ServerResponse } from '../server.port.js';\n\n/**\n * Server adapter that sends real HTTP requests via the Fetch API.\n * Used by the `e2e()` specification runner to hit a live server.\n */\nexport class FetchAdapter implements ServerPort {\n private baseUrl: string;\n\n constructor(url: string) {\n this.baseUrl = url.replace(/\\/$/, '');\n }\n\n async request(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record<string, string>,\n ): Promise<ServerResponse> {\n const init: RequestInit = {\n method,\n headers: { 'Content-Type': 'application/json', ...headers },\n };\n\n if (body !== undefined) {\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(`${this.baseUrl}${path}`, init);\n const responseBody = await response.json().catch(() => null);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n status: response.status,\n body: responseBody,\n headers: responseHeaders,\n };\n }\n}\n","import type { ServerPort, ServerResponse } from '../server.port.js';\n\n/**\n * Server adapter that dispatches requests in-process through a Hono app instance.\n * Used by the `integration()` specification runner -- no network overhead.\n */\nexport class HonoAdapter implements ServerPort {\n private app: {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n };\n\n constructor(app: {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n }) {\n this.app = app;\n }\n\n async request(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record<string, string>,\n ): Promise<ServerResponse> {\n const init: RequestInit = {\n method,\n headers: { 'Content-Type': 'application/json', ...headers },\n };\n\n if (body !== undefined) {\n init.body = JSON.stringify(body);\n }\n\n const response = await this.app.request(path, init);\n const responseBody = await response.json().catch(() => null);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n status: response.status,\n body: responseBody,\n headers: responseHeaders,\n };\n }\n}\n","import { existsSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\n/**\n * Resolve root — if relative, resolves from the caller's directory.\n */\nexport function resolveProjectRoot(root: string | undefined): string {\n if (!root) {\n return process.cwd();\n }\n\n if (isAbsolute(root)) {\n return root;\n }\n\n const stack = new Error('resolve root').stack;\n if (stack) {\n const lines = stack.split('\\n');\n for (const line of lines) {\n const match = line.match(/at\\s+(?:.*?\\()?(?:file:\\/\\/)?([^:)]+):\\d+:\\d+/);\n if (!match) {\n continue;\n }\n\n const filePath = match[1];\n if (filePath.includes('node_modules') || filePath.includes('/src/spec/')) {\n continue;\n }\n\n return resolve(filePath, '..', root);\n }\n }\n\n return resolve(process.cwd(), root);\n}\n\n/**\n * Resolve a CLI command — checks node_modules/.bin, then treats as absolute/PATH.\n */\nexport function resolveCommand(command: string, root: string): string {\n if (isAbsolute(command)) {\n return command;\n }\n\n const binPath = resolve(root, 'node_modules/.bin', command);\n if (existsSync(binPath)) {\n return binPath;\n }\n\n const cwdBinPath = resolve(process.cwd(), 'node_modules/.bin', command);\n if (existsSync(cwdBinPath)) {\n return cwdBinPath;\n }\n\n return command;\n}\n","import { DockerAssertion } from '../infra/docker/docker-assertion.js';\nimport { dockerContainer } from '../infra/docker/docker.js';\nimport { Orchestrator } from '../infra/orchestrator.js';\nimport {\n createSpecificationRunner,\n type DockerSpecConfig,\n type SeedHandler,\n type SpecificationBuilder,\n} from '../spec/builder.js';\nimport { ExecAdapter } from '../spec/modes/cli/adapters/exec.adapter.js';\nimport { FetchAdapter } from '../spec/modes/http/adapters/fetch.adapter.js';\nimport { HonoAdapter } from '../spec/modes/http/adapters/hono.adapter.js';\nimport type { DatabasePort } from '../spec/ports/database.port.js';\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\nimport { resolveCommand, resolveProjectRoot } from './resolve.js';\nimport type { AppTarget, CommandTarget, SpecTarget, StackTarget } from './targets.js';\n\n/** Shared options for all spec targets. */\nexport interface SpecOptions {\n /**\n * Opt-in Docker awareness for CLI-mode runners. When set, every\n * `.run()` generates a unique test-run id, injects it into the\n * child process env under `envVar`, and exposes `.container(name)`\n * accessors on the result that lazily query Docker. A test that\n * never calls `.container(...)` never touches the Docker daemon —\n * CLI-only and container-asserting tests share one runner.\n *\n * Always wrap calls that may spawn containers with `await using`\n * so leaked containers get force-removed at scope exit (the\n * runner cleans up by label filter).\n */\n docker?: DockerSpecConfig;\n /** Project root for fixture lookup and compose detection (relative paths supported). */\n root?: string;\n /**\n * Pluggable seed handlers for CLI-mode tests. Keys are leading path\n * segments (e.g. `\"spwn.yaml/\"`); values receive the seed context and the\n * absolute path of the source fragment under `<test-file-dir>/seeds/`.\n */\n seedHandlers?: Record<string, SeedHandler>;\n /** Infrastructure services to start via testcontainers. */\n services?: ServiceHandle[];\n /**\n * Optional normaliser applied to result.stdout.text and\n * result.stderr.text before every .toMatch / .toMatchFile\n * comparison. Useful for stripping ANSI codes, masking\n * machine-specific paths, scrubbing timestamps, etc.\n *\n * The transform does NOT mutate the raw `.text` accessor —\n * callers can still inspect the pristine output.\n */\n transform?: (text: string) => string;\n}\n\n/** A specification runner with teardown support and orchestrator access. */\nexport interface SpecRunner {\n /** The function to create individual specs: `runner('label').get('/path').run()`. */\n (label: string): SpecificationBuilder;\n /** Stop all infrastructure started by this runner. */\n cleanup: () => Promise<void>;\n /**\n * Get a Docker assertion builder for a running container.\n *\n * @example\n * await runner.docker('my-container').toBeRunning();\n */\n docker: (containerId: string) => DockerAssertion;\n /** The orchestrator managing the test infrastructure lifecycle. */\n orchestrator: Orchestrator;\n}\n\n/**\n * Create a specification runner for the given target.\n *\n * @param target - What to test against: {@link app}, {@link stack}, or {@link command}.\n * @param options - Shared options: root directory, infrastructure services.\n *\n * @example\n * // HTTP — in-process app with testcontainers\n * const s = await spec(app(() => createApp(db)), { services: [db] });\n *\n * // HTTP — full docker compose stack\n * const s = await spec(stack('../../'));\n *\n * // CLI — command binary\n * const s = await spec(command('my-cli'), { root: '../fixtures' });\n */\nexport async function spec(target: SpecTarget, options: SpecOptions = {}): Promise<SpecRunner> {\n switch (target.kind) {\n case 'app': {\n return startApp(target, options);\n }\n case 'stack': {\n return startStack(target, options);\n }\n case 'command': {\n return startCommand(target, options);\n }\n }\n}\n\n// ── Isolation helpers ──\n\nfunction getWorkerId(): string {\n return process.env.VITEST_POOL_ID ?? '0';\n}\n\nasync function acquireIsolation(services: ServiceHandle[]): Promise<void> {\n const workerId = getWorkerId();\n for (const service of services) {\n await service.isolation().acquire(workerId);\n }\n}\n\nasync function releaseIsolation(services: ServiceHandle[]): Promise<void> {\n for (const service of services) {\n await service.isolation().release();\n }\n}\n\n// ── Internal dispatchers ──\n\nasync function startApp(target: AppTarget, options: SpecOptions): Promise<SpecRunner> {\n const services = options.services ?? [];\n const orchestrator = new Orchestrator({\n mode: 'integration',\n root: resolveProjectRoot(options.root),\n services,\n });\n\n await orchestrator.start();\n await acquireIsolation(services);\n\n // Build services map keyed by composeName or type\n const servicesMap: Record<string, ServiceHandle> = {};\n for (const svc of services) {\n const key = svc.composeName ?? svc.type;\n servicesMap[key] = svc;\n }\n\n const factoryResult = target.factory(servicesMap);\n\n // Normalize: bare HonoApp or { server, jobs }\n // Check for 'jobs' array (not 'server') to discriminate — bare apps may have a 'server' property\n const isComposite = Array.isArray((factoryResult as any).jobs);\n const honoApp = isComposite ? (factoryResult as any).server : factoryResult;\n const jobs = isComposite ? (factoryResult as any).jobs : undefined;\n\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n jobs,\n server: new HonoAdapter(honoApp),\n }) as SpecRunner;\n\n runner.cleanup = async () => {\n await releaseIsolation(services);\n await orchestrator.stop();\n };\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n\nasync function startStack(target: StackTarget, options: SpecOptions): Promise<SpecRunner> {\n const root = resolveProjectRoot(target.root ?? options.root);\n const workerId = getWorkerId();\n const projectName = `test-worker-${workerId}`;\n\n const orchestrator = new Orchestrator({\n mode: 'e2e',\n root,\n services: options.services ?? [],\n projectName,\n });\n\n await orchestrator.startCompose();\n\n const appUrl = orchestrator.getAppUrl();\n if (!appUrl) {\n throw new Error(\n 'stack(): could not detect app URL from compose. Ensure an app service with ports is defined.',\n );\n }\n\n const database = orchestrator.getDatabase() ?? undefined;\n const databases = orchestrator.getDatabases();\n\n const runner = createSpecificationRunner({\n database,\n databases: databases.size > 0 ? databases : undefined,\n server: new FetchAdapter(appUrl),\n }) as SpecRunner;\n\n runner.cleanup = () => orchestrator.stopCompose();\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator;\n\n return runner;\n}\n\nasync function startCommand(target: CommandTarget, options: SpecOptions): Promise<SpecRunner> {\n const root = resolveProjectRoot(options.root);\n const bin = resolveCommand(target.bin, root);\n const services = options.services ?? [];\n\n let orchestrator: null | Orchestrator = null;\n let database: DatabasePort | undefined;\n let databases: Map<string, DatabasePort> | undefined;\n\n if (services.length) {\n orchestrator = new Orchestrator({\n mode: 'integration',\n root,\n services,\n });\n await orchestrator.start();\n await acquireIsolation(services);\n database = orchestrator.getDatabase() ?? undefined;\n const dbMap = orchestrator.getDatabases();\n databases = dbMap.size > 0 ? dbMap : undefined;\n }\n\n const runner = createSpecificationRunner({\n command: new ExecAdapter(bin),\n database,\n databases,\n dockerConfig: options.docker,\n fixturesRoot: root,\n seedHandlers: options.seedHandlers,\n transform: options.transform,\n }) as SpecRunner;\n\n runner.cleanup = async () => {\n await releaseIsolation(services);\n if (orchestrator) {\n await orchestrator.stop();\n }\n };\n runner.docker = (id: string) => new DockerAssertion(dockerContainer(id));\n runner.orchestrator = orchestrator!;\n\n return runner;\n}\n","/**\n * Target factories for {@link spec}. Each target describes what is being tested\n * and how the specification runner should connect to it.\n */\n\nimport type { ServiceHandle } from '../spec/ports/service.port.js';\n\n/** Any object with a request method compatible with Hono's app.request(). */\ntype HonoApp = {\n request: (path: string, init?: RequestInit) => Promise<Response> | Response;\n};\n\n/** A named job that can be triggered via .job(). */\nexport interface JobHandle {\n name: string;\n execute: () => Promise<void>;\n}\n\n/** Services map passed to the app factory after infrastructure starts. */\nexport type AppServices = Record<string, ServiceHandle>;\n\n/**\n * Return value from the app() factory. Either a bare server (backward compat)\n * or an object with server + jobs for job-based testing.\n */\nexport type AppFactoryResult = HonoApp | { server: HonoApp; jobs: JobHandle[] };\n\n// ── Target types ──\n\n/** In-process Hono app target. Created by {@link app}. */\nexport interface AppTarget {\n readonly kind: 'app';\n readonly factory: (services: AppServices) => AppFactoryResult;\n}\n\n/** Docker compose stack target. Created by {@link stack}. */\nexport interface StackTarget {\n readonly kind: 'stack';\n readonly root: string;\n}\n\n/** CLI command target. Created by {@link command}. */\nexport interface CommandTarget {\n readonly kind: 'command';\n readonly bin: string;\n}\n\n/** Any target that produces an HTTP interface (app or stack). */\nexport type HttpTarget = AppTarget | StackTarget;\n\n/** Any valid spec target. */\nexport type SpecTarget = AppTarget | CommandTarget | StackTarget;\n\n// ── Target factories ──\n\n/**\n * Test against an in-process Hono app. The factory receives started services\n * so you can wire connection strings into your app/DI container.\n *\n * @param factory - Function that receives services and returns a Hono app instance.\n *\n * @example\n * const db = postgres({ compose: 'db' });\n * await spec(\n * app((services) => createApp({ databaseUrl: services.db.connectionString })),\n * { services: [db] },\n * );\n */\nexport function app(factory: (services: AppServices) => HonoApp): AppTarget {\n return { kind: 'app', factory };\n}\n\n/**\n * Test against a full docker compose stack. The stack is started with\n * `docker compose up` and real HTTP requests are sent to the app service.\n *\n * @param root - Project root containing `docker/compose.test.yaml`.\n *\n * @example\n * await spec(stack('../../'));\n */\nexport function stack(root: string): StackTarget {\n return { kind: 'stack', root };\n}\n\n/**\n * Test a CLI binary. Each spec runs in a fresh temp directory.\n *\n * Pass a `docker: { envVar, nameLabel, testRunLabel }` option on\n * {@link SpecOptions} to make the runner Docker-aware — the runner\n * stamps a unique test-run id into `envVar`, and results expose\n * `.container(name)` accessors that lazily query Docker. Tests that\n * never call `.container(...)` pay zero Docker cost.\n *\n * @param bin - Path to the CLI binary or command name (resolved from node_modules/.bin or PATH).\n *\n * @example\n * // CLI-only\n * await spec(command('my-cli'), { root: '../fixtures' });\n *\n * // CLI binary that also spawns containers — same runner, just\n * // opt into container accessors via the docker option:\n * await spec(command('my-cli'), {\n * root: '../fixtures',\n * docker: {\n * envVar: 'MYCLI_TEST_LABEL',\n * nameLabel: 'com.mycli.world.name',\n * testRunLabel: 'com.mycli.test.run',\n * },\n * });\n */\nexport function command(bin: string): CommandTarget {\n return { kind: 'command', bin };\n}\n"],"mappings":"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"}