/** * KHAELOR * File: prototypes/tui-spike/driver/run.ts * Description: Headless measurement driver — runs a candidate inside a real pty (node-pty), * injects synthetic typing at 30 cps during streaming, resizes the pty mid-stream, * sends Esc to end the run, then collects the candidate's in-process metrics and * performs settled-content integrity checks on the captured output. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import pty from "node-pty"; import { stripAnsi } from "../shared/ansi"; import { FENCE_TEXT } from "../shared/demo"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const cand = process.argv[2]; if (cand !== "a" && cand !== "b") { console.error("usage: tsx driver/run.ts (DURATION_MS env, default 60000)"); process.exit(1); } const durationMs = Number(process.env.DURATION_MS ?? 60_000); const entry = cand === "a" ? "candidate-a/main.ts" : "candidate-b/main.tsx"; const metricsOut = path.join(root, "out", `metrics-${cand}.json`); const capturePath = path.join(root, "out", `capture-${cand}.txt`); fs.mkdirSync(path.join(root, "out"), { recursive: true }); if (fs.existsSync(metricsOut)) fs.unlinkSync(metricsOut); const p = pty.spawn(process.execPath, [path.join(root, "node_modules", "tsx", "dist", "cli.mjs"), entry], { name: "xterm-256color", cols: 100, rows: 30, cwd: root, env: { ...process.env, METRICS_OUT: metricsOut, DURATION_MS: String(durationMs), FORCE_COLOR: "1" }, }); let capture = ""; p.onData((d) => { capture += d; }); /* synthetic typing: 30 cps while the stream runs; Enter every 40 chars keeps the composer line short enough to survive the 60-column resize phase. */ const phrase = "check the session store retry logic and rerun the tests then verify the journal "; let typed = 0; let typeTimer: NodeJS.Timeout | null = null; setTimeout(() => { typeTimer = setInterval(() => { if (typed > 0 && typed % 40 === 0) p.write("\r"); p.write(phrase[typed % phrase.length]); typed++; }, 33); }, 1500); setTimeout(() => { if (typeTimer !== null) clearInterval(typeTimer); }, Math.max(2000, durationMs - 5000)); /* resize the pty mid-stream (delivers a real SIGWINCH + winsize change to the child) */ const resizePlan: Array<[number, number, number]> = [ [0.33, 120, 30], [0.42, 60, 30], [0.5, 200, 45], [0.58, 100, 30], ]; for (const [frac, c, r] of resizePlan) { setTimeout(() => { try { p.resize(c, r); } catch { /* child may have exited */ } }, Math.round(durationMs * frac)); } /* end of run: Esc, then hard kill as a backstop */ setTimeout(() => p.write("\x1b"), durationMs); const killer = setTimeout(() => p.kill(), durationMs + 8000); p.onExit(({ exitCode }) => { clearTimeout(killer); if (typeTimer !== null) clearInterval(typeTimer); fs.writeFileSync(capturePath, capture); const stripped = stripAnsi(capture).replace(/\r/g, ""); const fenceIntact = stripped.includes(FENCE_TEXT); const fullClearsInCapture = (capture.match(/\x1b\[[23]J/g) ?? []).length; let metrics: unknown = null; try { metrics = JSON.parse(fs.readFileSync(metricsOut, "utf8")); } catch { console.error(`no metrics file at ${metricsOut} — child exit code ${exitCode}`); } const summary = { candidate: cand, childExitCode: exitCode, durationMs, typedChars: typed, driverChecks: { settledFenceBlockByteIdentical: fenceIntact, fullScreenClearsInCapture: fullClearsInCapture, captureBytes: capture.length, capturePath, }, metrics, }; const summaryPath = path.join(root, "out", `summary-${cand}.json`); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + "\n"); console.log(JSON.stringify(summary, null, 2)); process.exit(0); });