Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import { cpSync, existsSync, 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 { ALLOWLIST_FILE, buildAllowlist } from "../runner/checkAllowlist";import { realRunDeps, runScriptFile, type RunDeps } from "../runner/run";import { buildExtension } from "./buildExtension";import { pinnedVersion } from "./chromePath";import { sabotage as breakExtension } from "./sabotage";import { checkIdentity, runSelfCheck, type Outcome, type SelfCheckResult, type SelfCheckScript } from "./selfCheck";
/** The port the fixture scripts are written for; each run gets its own, so scripts can run side by side. */const SCRIPT_PORT_ORIGIN = "http://127.0.0.1:4780";
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)); }); });
/** The fixture scripts: everything in fixtures/commands/scripts, and fixtures/hello.aite (the first end-to-end script). */export function loadScripts(root: string, only?: string): SelfCheckScript[] { const dir = join(root, "fixtures/commands/scripts"); const scripts: SelfCheckScript[] = readdirSync(dir) .filter((f) => f.endsWith(".aite")) .sort() .map((f) => ({ name: f.replace(/\.aite$/, ""), text: readFileSync(join(dir, f), "utf8") })); scripts.push({ name: "hello", text: readFileSync(join(root, "fixtures/hello.aite"), "utf8") }); return only === undefined ? scripts : scripts.filter((s) => s.name.includes(only));}
interface Project { dir: string; scriptPath: string; configPath: string; siteDir: string;}
/** A project in a temp folder, laid out like a real one: pages to serve, scripts/ with shared/ and checks/, and a config that allows the page's origin. */async function makeProject(root: string, name: string, text: string, chrome: string): Promise<Project> { const dir = mkdtempSync(join(tmpdir(), "aite-selfcheck-")); const port = await freePort(); const origin = `http://127.0.0.1:${port}`; const siteDir = join(dir, "site"); mkdirSync(siteDir); const commands = join(root, "fixtures/commands"); for (const f of readdirSync(commands, { withFileTypes: true })) if (f.isFile()) cpSync(join(commands, f.name), join(siteDir, f.name)); cpSync(join(root, "fixtures/hello.html"), join(siteDir, "hello.html")); mkdirSync(join(dir, "scripts"), { recursive: true }); cpSync(join(commands, "scripts/shared"), join(dir, "scripts/shared"), { recursive: true }); cpSync(join(commands, "checks"), join(dir, "scripts/checks"), { recursive: true }); // these fixture checks are in the repository, reviewed with it; the project the self-check builds lists them the way a person would (P7-5) writeFileSync(join(dir, "scripts/checks", ALLOWLIST_FILE), buildAllowlist(join(dir, "scripts/checks"))); const scriptPath = join(dir, "scripts", `${name}.aite`); writeFileSync(scriptPath, text.replaceAll(SCRIPT_PORT_ORIGIN, origin)); const configPath = join(dir, "aite.config.json"); writeFileSync(configPath, JSON.stringify({ profiles: { p: { userDataDir: join(dir, "profile"), chrome } }, allow: [origin] })); return { dir, scriptPath, configPath, siteDir };}
async function runInProject(root: string, name: string, text: string, chrome: string, breakCommand: string | undefined, launch?: RunDeps["launch"]): Promise<Outcome> { const project = await makeProject(root, name, text, chrome); try { const deps = { ...realRunDeps(), ...(launch !== undefined ? { launch } : {}), ...(breakCommand !== undefined ? { buildExtension: (o: { outdir: string; allow: string[] }) => { buildExtension(o); breakExtension(o.outdir, breakCommand, { read: (p) => readFileSync(p, "utf8"), write: (p, t) => writeFileSync(p, t), exists: existsSync }); } } : {}), }; // --no-sandbox: the Chrome sandbox cannot start in the containers this runs in (nor, as root, in CI) const r = await runScriptFile({ scriptPath: project.scriptPath, configPath: project.configPath, serveDir: project.siteDir, headless: true, noSandbox: true, connectTimeoutMs: 40_000 }, deps); let json: Outcome["json"]; try { const results = join(project.dir, "results"); json = JSON.parse(readFileSync(join(results, readdirSync(results)[0] ?? "", "run.json"), "utf8")); } catch { json = undefined; } return { exitCode: r.exitCode, output: r.output, ...(json !== undefined ? { json } : {}) }; } finally { rmSync(project.dir, { recursive: true, force: true }); }}
export interface RealSelfCheckOptions { root: string; chrome: string; repeat: number; parallel: number; sabotage?: string; only?: string; log?: (line: string) => void; /** A browser to use instead of launching Chrome (tests only: the self-check's own wiring is tested without a real Chrome). */ launch?: RunDeps["launch"]; /** The extension version to expect, when it is not this package's (a stand-in browser reports its own). */ extensionVersion?: string;}
const CANARY = '# expect: pass\ntest "canary"\n start http://127.0.0.1:4780/hello.html\n timeout 5s\n step "the extension answers"\n sleep 10ms\n';
export async function runRealSelfCheck(o: RealSelfCheckOptions): Promise<SelfCheckResult> { const scripts = loadScripts(o.root, o.only); return runSelfCheck(scripts, { repeat: o.repeat, parallel: o.parallel }, { /** Shows a Chrome of the pinned version is running with this build of the extension, before anything else is believed. The canary is never sabotaged. */ canary: async () => { const out = await runInProject(o.root, "canary", CANARY, o.chrome, undefined, o.launch); if (out.exitCode !== 0 || !out.json) throw new Error(`the canary run ended with exit ${out.exitCode}: ${out.output.split("\n").slice(0, 4).join(" | ")}`); const { chrome, extension } = out.json; const pinned = pinnedVersion(readFileSync(join(o.root, "package.json"), "utf8")); const wanted = o.extensionVersion ?? (JSON.parse(readFileSync(join(o.root, "package.json"), "utf8")).version as string); const problem = checkIdentity({ ...(chrome !== undefined ? { chrome } : {}), ...(extension !== undefined ? { extension } : {}) }, { chromePinned: pinned, extensionVersion: wanted }); if (problem) throw new Error(problem); return { chrome: chrome as string, extension: extension as string }; }, run: async (s, attempt) => { o.log?.(`run ${s.name} (${attempt + 1})`); return runInProject(o.root, s.name, s.text, o.chrome, o.sabotage, o.launch); }, });}