Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158// Run a guest program to exit on the fs fixture page and collect its stdio.import { test, expect, type Page } from "@playwright/test";import type { ExecOptions, Kernel } from "../src/host/index";import { listGoldens, maskTestDurations, readGolden, type GuestGolden } from "../scripts/golden-files.ts";
declare global { interface Window { __runtime?: Kernel }}
export interface GuestResult { code: number; signal: string | null; stdout: string; stderr: string;}
/** Like spawnSync, the guest's stdin gets `stdin` (or nothing) and is then closed. */export async function runGuest( page: Page, files: Record<string, string>, entry: string, stdin = "", options: ExecOptions = {},): Promise<GuestResult> { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); return await page.evaluate(async ({ files, entry, stdin, options }) => { const runtime = window.__runtime!; runtime.writeTree(files); const proc = runtime.exec(entry, options); const writer = proc.stdin.getWriter(); if (stdin) void writer.write(new TextEncoder().encode(stdin)); void writer.close(); const dec = new TextDecoder(); let stdout = ""; let stderr = ""; const piped = Promise.all([ proc.stdout.pipeTo(new WritableStream({ write(c) { stdout += dec.decode(c); } })), proc.stderr.pipeTo(new WritableStream({ write(c) { stderr += dec.decode(c); } })), ]); const code = await proc.exited; const signal = await proc.signal; await piped; return { code, signal, stdout, stderr }; }, { files, entry, stdin, options });}
/** Run a guest kept under tests/fixtures/guests/ (also runnable by real Node, which is * where each spec's expected lines come from) and return its stdout lines. */export async function guestFileLines(page: Page, name: string): Promise<string[]> { const res = await page.request.get(`/tests/fixtures/guests/${name}`); expect(res.ok()).toBe(true); return await guestLines(page, { [`/app/${name}`]: await res.text() }, `/app/${name}`);}
/** Run to a clean exit and return stdout lines. */export async function guestLines(page: Page, files: Record<string, string>, entry: string): Promise<string[]> { const res = await runGuest(page, files, entry); expect(res.stderr).toBe(""); expect(res.code).toBe(0); return res.stdout.trimEnd().split("\n");}
/** Every golden under tests/goldens/guests whose guest starts with one of `prefixes`, as one test * each: the kernel must print exactly what the pinned Node did. Record or re-check them * with `node scripts/golden.mjs`. */export function goldenSuite(prefixes: string[]): void { const guests = prefixes.flatMap((p) => listGoldens("guests", p)); test("the suite has goldens", () => expect(guests.length).toBeGreaterThan(0)); for (const guest of guests) goldenTest(readGolden("guests", guest));}
/** Run a golden's guest with its deps, the way it was recorded. */export async function runGoldenGuest(page: Page, g: GuestGolden): Promise<GuestResult> { const files: Record<string, string> = {}; for (const name of [g.guest, ...(g.deps ?? [])]) { const res = await page.request.get(`/tests/fixtures/guests/${name}`); expect(res.ok()).toBe(true); files[`/app/${name}`] = await res.text(); } return await runGuest(page, files, `/app/${g.guest}`, g.stdin);}
/** One golden as a test. With `fails`, the kernel is known not to match yet and the * test is expected to fail, so a fix shows up as a failure to delete the reason. */export function goldenTest(g: GuestGolden, fails?: string): void { test(`golden ${g.guest} matches Node`, async ({ page }) => { test.fail(fails !== undefined, fails); const out = await runGoldenGuest(page, g); // Narrow, opt-in masking (see GuestGolden#mask): only guests that declare it get // their durations/internal stack frames normalized, and both sides (the golden // recorded under real Node and the kernel's live output) go through the exact // same function so neither side is favored. const mask = (text: string): string => (g.mask ? maskTestDurations(text) : text); expect({ code: out.signal === null ? out.code : null, signal: out.signal, stdout: mask(out.stdout), stderr: mask(out.stderr.replace(/\(node:\d+\)/g, "(node:PID)")), }).toEqual({ code: g.code, signal: g.signal ?? null, stdout: mask(g.stdout), stderr: mask(g.stderr) }); });}
/** One step of a terminal session: wait until the master has shown `waitFor` (since the * previous wait), then type `write` or resize the terminal. */export type PtyStep = { waitFor?: string; write?: string; resize?: [cols: number, rows: number] };
export interface PtyResult { code: number; signal: string | null; /** Everything the master read: output and echo, after output processing. */ output: string; stderr: string;}
/** Run a guest on a pty of `size`, driving it with `steps`, until it exits. */export async function runPtyGuest( page: Page, source: string, steps: PtyStep[], options: { size?: [number, number]; env?: Record<string, string> } = {},): Promise<PtyResult> { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); return await page.evaluate(async ({ source, steps, size, env }) => { const runtime = window.__runtime!; runtime.writeTree({ "/app/main.cjs": source }); const proc = runtime.exec("/app/main.cjs", { pty: { cols: size[0], rows: size[1] }, env }); const dec = new TextDecoder(); let output = ""; let stderr = ""; let seen = 0; const piped = Promise.all([ proc.stdout.pipeTo(new WritableStream({ write(c) { output += dec.decode(c); } })), proc.stderr.pipeTo(new WritableStream({ write(c) { stderr += dec.decode(c); } })), ]); let exited = false; void proc.exited.then(() => { exited = true; }); for (const step of steps) { if (step.waitFor !== undefined) { const text = step.waitFor; await new Promise<void>((resolve, reject) => { const started = Date.now(); const check = (): void => { const at = output.indexOf(text, seen); if (at !== -1) { seen = at + text.length; resolve(); } else if (exited || Date.now() - started > 5000) { reject(new Error(`never saw ${JSON.stringify(text)}; output so far ${JSON.stringify(output)}`)); } else setTimeout(check, 5); }; check(); }); } if (step.write !== undefined) proc.pty.write(step.write); if (step.resize) proc.pty.resize(...step.resize); } const code = await proc.exited; const signal = await proc.signal; await piped; return { code, signal, output, stderr }; }, { source, steps, size: options.size ?? [80, 24], env: options.env ?? {} });}