/** * Evidence recording. Every check attaches what it actually observed, so a * green run is readable afterwards and a red run is diagnosable without a * re-run. * * This is the harness half of the project's observability rule: a process that * discards its own output is a defect, and a test that asserts without * recording what it saw is the same defect wearing a different hat. */ import { test, type TestInfo } from '@playwright/test'; import { redact } from './env'; import type { CommandResult } from './cluster'; /** Human-readable transcript of one command, for attachment. */ function transcript(result: CommandResult): string { const lines = [`$ ${result.command}`, `exit: ${result.code}`]; if (result.stdout.trim() !== '') lines.push('--- stdout ---', result.stdout.trimEnd()); if (result.stderr.trim() !== '') lines.push('--- stderr ---', result.stderr.trimEnd()); return `${lines.join('\n')}\n`; } /** Attach one command's full transcript under a stable name. */ export async function recordCommand( testInfo: TestInfo, name: string, result: CommandResult, ): Promise { await testInfo.attach(`${name}.txt`, { body: Buffer.from(transcript(result)), contentType: 'text/plain', }); } /** Attach an arbitrary observation as JSON, redacted. */ export async function recordJson( testInfo: TestInfo, name: string, value: unknown, ): Promise { await testInfo.attach(`${name}.json`, { body: Buffer.from(redact(JSON.stringify(value, null, 2))), contentType: 'application/json', }); } /** * Run a command inside a named `test.step` and attach its transcript, whether * it succeeded or not. Returns the result so the caller can assert on it. * * Steps are what make the HTML report read as a walkthrough of the runbook * rather than a flat list of assertions. */ export async function observed( testInfo: TestInfo, label: string, invoke: () => Promise, ): Promise { return test.step(label, async () => { const result = await invoke(); await recordCommand(testInfo, label.replace(/[^a-z0-9]+/gi, '-').toLowerCase(), result); return result; }); }