import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { realRunDeps, runScriptFile } from "../runner/run"; import { CASES, type PanelCase, type Truth } from "./panelCases"; /** * Makes the reader kit for the cold-reader panel: one report.html per case, from real runs in a real Chrome, shuffled and numbered * neutrally, plus answers.json (the truth of each, kept apart from the reports). Readers are given only the report-N.html files. */ export interface KitEntry { file: string; case: string; truth: Truth; why: string; /** What aite itself said about the failed step, for the person scoring (never shown to readers). */ failure: { reason: string; step: string }; } const freePort = () => new Promise((resolve) => { const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address() as { port: number }; s.close(() => resolve(port)); }); }); /** A small deterministic shuffle (mulberry32), so the same seed gives the same order. */ export function shuffled(items: T[], seed: number): T[] { let a = seed >>> 0; const next = () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; const out = [...items]; for (let i = out.length - 1; i > 0; i--) { const j = Math.floor(next() * (i + 1)); [out[i], out[j]] = [out[j]!, out[i]!]; } return out; } export async function makeKit(options: { chrome: string; outDir: string; seed?: number; cases?: PanelCase[]; noSandbox?: boolean }): Promise { const cases = shuffled(options.cases ?? CASES, options.seed ?? Date.now()); mkdirSync(options.outDir, { recursive: true }); const entries: KitEntry[] = []; for (const [i, c] of cases.entries()) { const n = i + 1; const dir = mkdtempSync(join(tmpdir(), "aite-run-")); try { const origin = `http://127.0.0.1:${await freePort()}`; mkdirSync(join(dir, "fixtures"), { recursive: true }); // the page is the same file for both versions: ?v=a or ?v=b picks the body (a script in the page cannot know which case it is) const pageName = `p${n}.html`; writeFileSync(join(dir, "fixtures", pageName), ``); // probes off: the checks of the machine and the network read this host (its load, its gateway) and would put different environment noise in every report; // the panel is about how a reader takes in what was wanted, what was found and why writeFileSync(join(dir, "aite.config.json"), JSON.stringify({ profiles: { p: { userDataDir: join(dir, "profile"), chrome: options.chrome } }, allow: [origin], diagnosis: { probes: false } })); const script = (v: string) => `test "send money"\n start ${origin}/${pageName}?v=${v}\n timeout 2s\n${c.steps}`; const run = (v: string) => { writeFileSync(join(dir, "fixtures/send.aite"), script(v)); return runScriptFile({ scriptPath: join(dir, "fixtures/send.aite"), configPath: join(dir, "aite.config.json"), headless: true, noSandbox: options.noSandbox ?? true, connectTimeoutMs: 40_000 }, realRunDeps()); }; const base = await run("a"); if (base.exitCode !== 0) throw new Error(`case ${c.id}: the baseline run did not pass (exit ${base.exitCode})\n${base.output}`); const judged = await run("b"); if (judged.exitCode !== 1) throw new Error(`case ${c.id}: the run to judge was meant to fail (exit ${judged.exitCode})\n${judged.output}`); const runDir = /^results: (.+)$/m.exec(judged.output)![1]!; const json = JSON.parse(readFileSync(join(runDir, "run.json"), "utf8")) as { tests: Array<{ steps: Array<{ name: string; status: string; commands: Array<{ reason?: string }> }> }> }; const failed = json.tests[0]!.steps.find((s) => s.status === "failed")!; const file = `report-${n}.html`; writeFileSync(join(options.outDir, file), readFileSync(join(runDir, "report.html"))); entries.push({ file, case: c.id, truth: c.truth, why: c.why, failure: { reason: failed.commands.find((x) => x.reason)?.reason ?? "?", step: failed.name } }); } finally { rmSync(dir, { recursive: true, force: true }); } } writeFileSync(join(options.outDir, "answers.json"), JSON.stringify(entries, null, 2) + "\n"); return entries; } /** The files to hand to readers: the reports and nothing else. */ export const readerFiles = (outDir: string): string[] => readdirSync(outDir).filter((f) => /^report-\d+\.html$/.test(f)).sort((a, b) => Number(/\d+/.exec(a)![0]) - Number(/\d+/.exec(b)![0]));