Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { 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<number>((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<T>(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<KitEntry[]> { 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), `<!doctype html><html><head><meta charset="utf-8"></head><body><script>var v=new URLSearchParams(location.search).get("v");var pages=${JSON.stringify({ a: c.a, b: c.b }).replace(/</g, "\\u003c")};document.open();document.write(pages[v]||pages.a);document.close();</script></body></html>`); // 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]));