// Create, end to end, in the host's browser: a recorded model stream fed to the page, the // app rendering as it arrives, its room joined on a real room service that `wai dev` runs, a // poll opened and answered, the app taken away as app.oui and made a project by // `wai create --from app.oui`, and that project checked and run with `wai dev`. // // node create.e2e.mjs --rt --relay --room // --wai --web --out // [--browser ] [--site ] // // With --site, the WAI site's build is served instead of the runtime's directory, and its // /create/ page is driven: the page as the site serves it, with its runtime under /wai-app/. // // Scenarios, in order: // create the `show` recording replayed into Create, served by `wai dev --show` (its relay, // its room service): widgets appear while the stream is still arriving and only grow // in number; every snapshot's tree validates; the room widget's frame loads the room // client while later lines still stream; the host joins (the room's clock records // it), sends a chat, opens the poll; the page's sealed log holds each action and // verifies; Export hands over app.oui, the program in its canonical form, and the // command that makes it a project; `wai create --from app.oui` writes the project // built the exported project: `wai app check` passes, and refuses it with its room not // its app's, another audience, no room named, a runtime file changed, or a refused // line; `wai dev` in the project runs its own relay and room service from wai.json, // its keys in .wai/ (which .gitignore keeps out and the front never serves); there a // host joins, chats and opens the poll, a viewer watches, reads the chat and votes, // and both pages show the result the room's clock signed // refused the `watch-refused` recording: the Chat and Poll a watch room cannot keep never // render, each refused `disallowed-child` at its line, and export stays off // own the visitor's own model: a stand-in chat/completions server on loopback streams // the room recording; the page renders it, and the key it was given goes to that // endpoint alone, in its Authorization header, and is stored nowhere // agent the visitor's coding agent: `wai mcp --toolset app` driven over its stdio streams // the show into the page through its bridge, the page connected with the pairing // code; `app_export` writes the project `wai create --from` writes // // Every wait is bounded; nothing listens beyond loopback. import fs from "node:fs"; import path from "node:path"; import net from "node:net"; import { createRequire } from "node:module"; import { spawn, execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const LIB = path.resolve(HERE, "../../../joule-code/joule-code-rs/crates/joule-moq-web/e2e/lib.mjs"); const { serveSameOrigin, browserArgs, memguardedBrowser, killStrays, until, sleep } = await import(LIB); const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => (x.startsWith("--") ? [...a, [x.slice(2), all[i + 1] && !all[i + 1].startsWith("--") ? all[i + 1] : "1"]] : a), [])); const need = (k) => { if (!args[k]) { console.error(`create e2e: --${k} is required`); process.exit(2); } return path.resolve(args[k]); }; const RELAY = need("relay"), ROOM = need("room"), WAI = need("wai"), WEB = need("web"); // The runtime's directory (e2e/run.sh lays it out), served as the site with Create's // development page at /create/index.html; or, with --site, a WAI site build, whose /create/ // page loads the runtime from /wai-app/. const SITE = args.site ? path.resolve(args.site) : null; const RT = SITE ? path.join(SITE, "wai-app") : need("rt"); const ROOT = SITE || RT; const CREATE = SITE ? "/create/" : "/create/index.html"; const OUT = need("out"); const ONLY = (args.only || "create,built,refused,own,agent").split(","); // Each run starts from no results: the room service's keygen refuses a key file a previous // run left. fs.rmSync(OUT, { recursive: true, force: true }); fs.mkdirSync(OUT, { recursive: true }); const require = createRequire(path.join(process.env.PLAYWRIGHT_HOST || path.join(HERE, "node_modules"), "x")); const { chromium } = require("playwright"); const STEP = 60000; let failed = 0; const results = []; function check(name, ok, detail = "") { results.push({ name, ok, detail }); if (!ok) failed++; console.log(`${ok ? "ok " : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`); } process.on("unhandledRejection", (e) => { console.error("unhandled:", (e && e.stack) || e); killStrays(); process.exit(1); }); const freePort = () => new Promise((ok) => { const s = net.createServer(); s.listen(0, "127.0.0.1", () => { const p = s.address().port; s.close(() => ok(p)); }); }); const procs = []; function run(bin, argv, name, dir, env = {}) { const log = fs.openSync(path.join(dir, `${name}.log`), "w"); const p = spawn(bin, argv, { stdio: ["ignore", log, log], env: { ...process.env, NO_COLOR: "1", ...env }, cwd: env.CWD || dir }); p.logFile = path.join(dir, `${name}.log`); procs.push(p); return p; } const tail = (f) => { try { return fs.readFileSync(f, "utf8").split("\n").slice(-8).join(" | "); } catch { return ""; } }; // `wai` in `cwd`: its exit status and its output. const wai = (argv, cwd) => { try { return { code: 0, out: execFileSync(WAI, argv, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString() }; } catch (e) { return { code: e.status, out: `${e.stdout || ""}${e.stderr || ""}` }; } }; const envelope = (out) => JSON.parse(out.split("\n").find((l) => l.startsWith("{")) || "{}"); // `wai dev` in `cwd` (a project, or with --site a directory of pages), on a free port: its // relay and room service, with this host's binaries and room client. Resolves once it is ready. async function waiDev(cwd, name, extra = []) { const port = await freePort(); const dir = path.join(OUT, name); fs.mkdirSync(dir, { recursive: true }); const p = run(WAI, ["dev", "--port", String(port), "--web", WEB, "--relay", RELAY, "--room", ROOM, ...extra], name, dir, { CWD: cwd }); await until(`wai dev (${name}) is ready`, 90000, () => { if (p.exitCode != null) throw new Error(`wai dev exited: ${tail(p.logFile)}`); return /wai dev: ready on/.test(fs.readFileSync(p.logFile, "utf8")); }, 300); return { port, url: `http://127.0.0.1:${port}`, proc: p, dir }; } let browser; async function launch() { const bin = args.browser ? path.resolve(args.browser) : null; const opts = { headless: true, args: browserArgs() }; if (bin) opts.executablePath = memguardedBrowser(bin, OUT).executable; browser = await chromium.launch(opts); console.log(`browser: ${browser.version()}`); } async function page(url, name) { const ctx = await browser.newContext({ permissions: ["camera", "microphone"], acceptDownloads: true }); const p = await ctx.newPage(); const logs = []; p.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); p.on("pageerror", (e) => logs.push(`[pageerror] ${e.stack || e}`)); p.on("response", (r) => { if (r.status() >= 400) logs.push(`[http ${r.status()}] ${r.url()}`); }); p.dump = () => fs.writeFileSync(path.join(OUT, `${name}.console.log`), logs.join("\n")); p.logs = logs; await p.goto(url, { waitUntil: "domcontentloaded", timeout: STEP }); return p; } const roomOf = (p) => p.evaluate(() => { const r = (window.createTest || window.appTest).ctx.room.current; return r ? { phase: r.phase(), me: r.me(), co: r.co(), chats: r.chats(), checked: r.checked(), present: r.present() } : null; }); const recording = (n) => JSON.parse(fs.readFileSync(path.join(RT, "create/recorded", `wai-app-${n}.json`), "utf8")); const SHOW = recording("show"); const SHOW_TEXT = SHOW.chunks.join(""); let shared = null; // the Create page's `wai dev`, the page, the exported project async function scenarioCreate() { const dir = path.join(OUT, "create"); fs.mkdirSync(dir, { recursive: true }); // The Create page, served by `wai dev` with a show's room: the site is the runtime. const dev = await waiDev(ROOT, "create-dev", ["--site", ROOT, "--show", "--audience", "1", "--dir", path.join(OUT, "create-dev", "state")]); check("create: wai dev runs a show's relay and room service behind the Create page", true, dev.url); const srv = { url: dev.url, close: async () => {} }; const p = await page(`${srv.url}${CREATE}`, "create-host"); await until("Create loads its session", STEP, () => p.evaluate(() => !!window.createTest)); // Watch the page while the stream arrives: when each widget appears, and how much of the // program had arrived then. await p.evaluate(() => { window.__seen = []; new MutationObserver(() => { for (const el of document.querySelectorAll("#app [data-widget]")) { if (el.__seen) continue; el.__seen = true; window.__seen.push({ widget: el.dataset.widget, key: el.dataset.key, chars: window.createTest.session.program().length, state: document.getElementById("create").dataset.state }); } }).observe(document.getElementById("app"), { childList: true, subtree: true }); }); await p.selectOption("#recording", "show"); await p.click("#build"); await until("the stream ends", STEP, () => p.evaluate(() => window.createTest.done >= 1)); const snaps = await p.evaluate(() => window.createTest.snapshots); const seen = await p.evaluate(() => window.__seen); const total = SHOW_TEXT.length; const counts = snaps.map((s) => s.widgets); check("create: the app renders while the stream arrives, the first widget after a fraction of it", seen.length >= 8 && seen[0].state === "streaming" && seen[0].chars < total / 4, `${seen.length} widgets; first ${seen[0] && seen[0].widget} at ${seen[0] && seen[0].chars} of ${total} characters`); check("create: widgets appear in steps over the stream, never fewer than before", counts.every((c, i) => i === 0 || c >= counts[i - 1]) && new Set(counts).size >= 6 && seen.filter((x) => x.state === "streaming").length >= 6, `${snaps.length} snapshots; counts ${[...new Set(counts)].join(" → ")}`); const room = seen.find((x) => x.widget === "Show"); check("create: the room widget (and its room client) appears while later lines still stream", !!room && room.chars < total / 2, room ? `Show at ${room.chars} of ${total}` : "no Show"); const fin = await p.evaluate(() => window.createTest.last); check("create: the final snapshot has no error, one show, eight widgets", fin.finished && fin.errors.length === 0 && fin.room && fin.room.kind === "show" && fin.nodes.length === 8, JSON.stringify({ errors: fin.errors, room: fin.room && fin.room.kind, n: fin.nodes.length })); // The host joins the show: the room client in its frame enters the room service's room. await until("the room client is ready", STEP, () => p.evaluate(() => { const r = window.createTest.ctx.room.current; return r && r.phase() === "idle"; }), 300); await p.click('[data-widget="Show"] [data-act="join"]'); const host = await until("the host is in the room, publishing", STEP, async () => { const r = await roomOf(p); return r && r.phase === "publishing" && r; }, 300); const joined = await until("the room's clock records the host joining", STEP, () => p.evaluate((me) => { const s = window.createTest.ctx.room.current.state(); return s.timeline.some((e) => e.kind === "join" && e.who === "room") && (s.room && s.room.present || []).some((x) => x.principal === me); }, host.me), 300); check("create: the host joins a real room: the room service granted its key, the relay took its proof, the room's clock recorded it", !!joined, `principal ${host.me.slice(0, 16)}…`); // A chat, sent from the Chat widget, signed and verified back into the room's record. await p.fill('[data-widget="Chat"] input', "Welcome to the show"); await p.click('[data-widget="Chat"] button[type="submit"]'); await until("the host's chat is in the room's verified record", STEP, async () => (await roomOf(p)).chats.some((c) => c.text === "Welcome to the show"), 300); check("create: a chat sent from the widget comes back as a verified event of the room", true); // The lead opens the poll. await until("the host leads the show", STEP, async () => { const r = await roomOf(p); return r.co && r.co.lead; }, 300); await p.click('[data-widget="Poll"] button.wai-primary'); const w = await until("the room's clock opens the poll's window", STEP, async () => { const r = await roomOf(p); return r.co && r.co.windows.find((x) => x.question === "Which song next?"); }, 300); check("create: the poll opened from the widget is a window the room's clock signed", !!w, `window ${w.digest.slice(0, 16)}…, closes at ${w.close}`); // The page's sealed log: every action, each verified as a link of the chain. const log = await p.evaluate(() => ({ log: window.createTest.session.log(), v: window.createTest.session.verify(), actor: window.createTest.session.actor })); const acts = log.log.map((e) => e.action); check("create: the page sealed each action into its hash-chained log, which verifies", ["room.join", "chat.send", "poll.open"].every((a) => acts.includes(a)) && log.v.verified === log.log.length && log.log.every((e) => e.actor === log.actor && e.energy.provenance === "Estimator"), `${acts.join(", ")}; verified ${log.v.verified}`); const shown = await p.evaluate(() => document.querySelector('[data-widget="Receipts"] .wai-verdict').dataset.ok); check("create: the Receipts widget shows the page's own verification", shown === "true"); const cut = await p.evaluate(async () => { const raw = JSON.parse(window.createTest.session.logRaw()); raw.splice(0, 1); const at = new URL("./wai-app-session.js", new URL(document.getElementById("create").dataset.runtime, location.href)); return (await import(at.href)).verifyLog(JSON.stringify(raw)); }); check("create: the log with its first link cut out does not verify", JSON.parse(cut).broken_at === 0, cut); // Export: the program, as app.oui, and the command that makes it a project. const dl = p.waitForEvent("download", { timeout: STEP }); await p.click("#export"); const d = await dl; const oui = path.join(dir, "app.oui"); await d.saveAs(oui); const canonical = await p.evaluate(() => window.createTest.session.canonical()); const command = await p.evaluate(() => document.getElementById("export-command").textContent); check("create: Export hands over app.oui, the program in its canonical form, and the command that makes it a project", d.suggestedFilename() === "app.oui" && fs.readFileSync(oui, "utf8") === canonical && /^wai create [a-z0-9-]+ --from app\.oui$/.test(command), command); const project = path.join(dir, "exported"); const made = wai(["create", "exported", "--from", "app.oui", "--runtime", RT], dir); const files = made.code === 0 ? fs.readdirSync(project).sort() : []; const wj = made.code === 0 ? JSON.parse(fs.readFileSync(path.join(project, "wai.json"), "utf8")) : { dev: {} }; check("create: `wai create --from app.oui` writes the project: the program, wai.json naming its show, the page, its runtime, README, AGENTS.md and LICENSE", made.code === 0 && ["app.oui", "wai.json", "index.html", "README.md", "AGENTS.md", "LICENSE", "vendor.json", "vendor", ".gitignore"].every((f) => files.includes(f)) && wj.app === "app.oui" && wj.dev.mode === "show" && wj.dev.audience === 1 && fs.readFileSync(path.join(project, "app.oui"), "utf8") === canonical, made.code === 0 ? `${files.join(" ")}; dev ${JSON.stringify(wj.dev)}` : made.out); const vendor = made.code === 0 ? JSON.parse(fs.readFileSync(path.join(project, "vendor.json"), "utf8")) : { files: [] }; check("create: the project carries the runtime under vendor/, and no key", vendor.files.length > 10 && !fs.readdirSync(project).some((f) => /key/i.test(f)), `${vendor.files.length} runtime files`); shared = { dev, srv, p, project, window: w, dir }; } async function scenarioBuilt() { if (!shared) return check("built: needs the create scenario", false); const { project, dir } = shared; const ok = wai(["app", "check", "--json"], project); const okv = envelope(ok.out); check("built: `wai app check` passes on the exported project", ok.code === 0 && okv.ok === true, ((okv.data && okv.data.checked) || []).join("; ")); // Negative controls: the check refuses a project that is not whole. const broken = path.join(dir, "exported-broken"); const edit = (f, from, to) => { const t = fs.readFileSync(f, "utf8"); if (!from.test(t)) throw new Error(`${f}: ${from}`); fs.writeFileSync(f, t.replace(from, to)); }; for (const [what, mutate, expect] of [ ["whose room is not its app's", (d) => edit(path.join(d, "wai.json"), /"mode": "show", "audience": 1/, '"mode": "room"'), /dev\.mode is "room"/], ["whose show has another audience", (d) => edit(path.join(d, "wai.json"), /"audience": 1/, '"audience": 7'), /dev\.audience is 7/], ["that names no room", (d) => edit(path.join(d, "wai.json"), /, "mode": "show", "audience": 1/, ""), /names no room/], ["with a runtime file changed", (d) => fs.appendFileSync(path.join(d, "vendor/create/wai-app-components.js"), "\n// changed\n"), /wai-app-components\.js: its SHA-256/], ["with a line the registry refuses", (d) => edit(path.join(d, "app.oui"), /^(\w+) = Chat\(.*\)$/m, "$1 = Hologram()"), /line \d+: unknown-component/], ]) { fs.rmSync(broken, { recursive: true, force: true }); fs.cpSync(project, broken, { recursive: true }); mutate(broken); const r = wai(["app", "check", "--json"], broken); const v = envelope(r.out); const failures = ((v.data && v.data.failures) || []).join(" | "); check(`built: \`wai app check\` refuses a project ${what}`, r.code === 1 && expect.test(failures), failures.slice(0, 200)); } // The project's own development setup: `wai dev` in it, from its wai.json. const dev = await waiDev(project, "exported-dev"); const keys = path.join(project, ".wai/dev/room-keys.toml"); const leaked = await fetch(`${dev.url}/.wai/dev/room-keys.toml`).then((r) => r.status).catch((e) => String(e)); check("built: wai dev makes the room's keys in .wai/, which .gitignore keeps out and the front never serves", fs.existsSync(keys) && fs.readFileSync(path.join(project, ".gitignore"), "utf8").includes(".wai/") && leaked !== 200, `GET /.wai/dev/room-keys.toml: ${leaked}`); const h = await page(`${dev.url}/`, "exported-host"); await until("the exported app renders", STEP, () => h.evaluate(() => window.appTest && document.querySelectorAll("#wai-app [data-widget]").length === 8)); check("built: the exported app renders its eight widgets from app.oui", true); await until("its room client is ready", STEP, () => h.evaluate(() => { const r = window.appTest.ctx.room.current; return r && r.phase() === "idle"; }), 300); await h.click('[data-widget="Show"] [data-act="join"]'); await until("the host is in the project's room, publishing", STEP, async () => { const r = await roomOf(h); return r && r.phase === "publishing" && r; }, 300); await h.fill('[data-widget="Chat"] input', "Welcome to the show"); await h.click('[data-widget="Chat"] button[type="submit"]'); await until("the host's chat is in the room's verified record", STEP, async () => (await roomOf(h)).chats.some((c) => c.text === "Welcome to the show"), 300); await until("the host leads the show", STEP, async () => { const r = await roomOf(h); return r.co && r.co.lead; }, 300); const v = await page(`${dev.url}/`, "exported-viewer"); await until("the viewer's app renders", STEP, () => v.evaluate(() => window.appTest && document.querySelectorAll("#wai-app [data-widget]").length === 8)); await until("its room client is ready", STEP, () => v.evaluate(() => { const r = window.appTest.ctx.room.current; return r && r.phase() === "idle"; }), 300); await v.click('[data-widget="Show"] [data-act="watch"]'); const viewer = await until("the viewer watches", STEP, async () => { const r = await roomOf(v); return r && r.phase === "watching" && r; }, 300); check("built: in the project's own room, a host joins and a viewer enters", !!viewer.me, viewer.me.slice(0, 16)); await until("the viewer reads the host's chat", STEP, async () => (await roomOf(v)).chats.some((c) => c.text === "Welcome to the show"), 300); check("built: the viewer reads the host's chat, verified", true); await h.click('[data-widget="Poll"] button.wai-primary'); await until("a window open now", STEP, async () => { const x = await roomOf(v); return x.co && x.co.windows.find((y) => y.question === "Which song next?" && y.close > x.co.now); }, 300); await until("the viewer's poll offers its options", STEP, () => v.evaluate(() => [...document.querySelectorAll('[data-widget="Poll"] button.wai-option')].some((b) => !b.disabled)), 300); await v.click('[data-widget="Poll"] button.wai-option[data-option="1"]'); const chosen = await until("the room's clock selects and signs the result", STEP * 2, async () => { const t = await v.evaluate(() => document.querySelector('[data-widget="Poll"] .wai-poll-result').textContent); return /chose “The loud one”, with 1 vote/.test(t) && t; }, 500); const hostSees = await until("the host's page shows the same result", STEP, async () => { const t = await h.evaluate(() => document.querySelector('[data-widget="Poll"] .wai-poll-result').textContent); return /The loud one/.test(t) && t; }, 500); check("built: the viewer's vote is selected by the room's clock, and both pages show it", !!chosen && !!hostSees, chosen); const vlog = await v.evaluate(() => ({ acts: window.appTest.session.log().map((e) => e.action), v: window.appTest.session.verify() })); check("built: the exported app sealed the viewer's actions too", vlog.acts.includes("room.watch") && vlog.acts.includes("poll.vote") && vlog.v.verified === vlog.acts.length, vlog.acts.join(", ")); h.dump(); v.dump(); dev.proc.kill("SIGTERM"); } async function scenarioRefused() { const srv = shared ? shared.srv : await serveSameOrigin(ROOT, { service: "http://127.0.0.1:9", relayPort: 9 }); const p = await page(`${srv.url}${CREATE}`, "create-refused"); await until("Create loads", STEP, () => p.evaluate(() => !!window.createTest)); await p.evaluate(() => { window.__ever = new Set(); new MutationObserver(() => { for (const el of document.querySelectorAll("#app [data-widget]")) window.__ever.add(el.dataset.widget); }).observe(document.getElementById("app"), { childList: true, subtree: true }); }); await p.selectOption("#recording", "watch-refused"); await p.click("#build"); await until("the stream ends", STEP, () => p.evaluate(() => window.createTest.done >= 1)); const ever = await p.evaluate(() => [...window.__ever]); const fin = await p.evaluate(() => window.createTest.last); const errs = fin.errors.map((e) => `${e.line}:${e.code}:${e.component}`); check("refused: the Chat and the Poll a watch room cannot keep never render", !ever.includes("Chat") && !ever.includes("Poll") && ever.includes("WatchTogether") && ever.includes("Participants"), ever.join(", ")); check("refused: each is refused disallowed-child at its line", errs.includes("3:disallowed-child:Chat") && errs.includes("4:disallowed-child:Poll"), errs.join(" ")); const listed = await p.evaluate(() => [...document.querySelectorAll("#problems li")].map((li) => li.dataset.code)); const off = await p.evaluate(() => document.getElementById("export").disabled); check("refused: the page lists the refusals, and export stays off", listed.filter((c) => c === "disallowed-child").length === 2 && off, listed.join(" ")); if (!shared) await srv.close(); } // The visitor's own model: a stand-in `chat/completions` server on loopback (another origin // than the page's) that streams the room recording as a model would, token by token. The key // typed in the page goes to that endpoint alone, in its Authorization header, and is stored // nowhere. async function scenarioOwn() { const dir = path.join(OUT, "own"); fs.mkdirSync(dir, { recursive: true }); const rec = recording("room"); const KEY = "test-key-0f3a9c"; const seen = []; const http = await import("node:http"); const model = http.createServer((req, res) => { const cors = { "access-control-allow-origin": req.headers.origin || "*", "access-control-allow-headers": "authorization, content-type", "access-control-allow-methods": "POST" }; if (req.method === "OPTIONS") { res.writeHead(204, cors); return res.end(); } let body = ""; req.on("data", (d) => { body += d; }); req.on("end", async () => { seen.push({ path: req.url, auth: req.headers.authorization, body: JSON.parse(body || "{}") }); res.writeHead(200, { ...cors, "content-type": "text/event-stream" }); res.write('data: {"choices":[{"delta":{"role":"assistant"}}]}\n\n'); for (const c of rec.chunks) { res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: c } }] })}\n\n`); await sleep(5); } res.end("data: [DONE]\n\n"); }); }); await new Promise((ok) => model.listen(0, "127.0.0.1", ok)); const endpoint = `http://127.0.0.1:${model.address().port}/v1/chat/completions`; const srv = shared ? shared.srv : await serveSameOrigin(ROOT, { service: "http://127.0.0.1:9", relayPort: 9 }); const p = await page(`${srv.url}${CREATE}`, "create-own"); const requests = []; p.on("request", (r) => requests.push({ url: r.url(), auth: r.headers().authorization || null })); await until("Create loads", STEP, () => p.evaluate(() => !!window.createTest)); await p.check('input[name="source"][value="own"]'); await p.fill("#own-endpoint", endpoint); await p.fill("#own-model", "a-local-model"); await p.fill("#own-key", KEY); await p.fill("#prompt", rec.prompt); await p.click("#build"); await until("the stream from the visitor's model ends", STEP, () => p.evaluate(() => window.createTest.done >= 1)); const fin = await p.evaluate(() => window.createTest.last); const says = await p.evaluate(() => document.getElementById("model-says").textContent); check("own: the page streams the app from the visitor's own model and renders it", fin.errors.length === 0 && fin.room && fin.room.kind === "live" && fin.nodes.length === 6, `${fin.nodes.length} widgets; "${says.slice(0, 90)}…"`); const call = seen[0] || {}; check("own: the model is asked with the registry's guide as its instructions and the person's description", call.body && call.body.stream === true && call.body.model === "a-local-model" && /You build a WAI app/.test(call.body.messages[0].content) && call.body.messages[1].content === rec.prompt); const withKey = requests.filter((r) => r.auth && r.auth.includes(KEY)).map((r) => r.url); const stored = await p.evaluate((k) => JSON.stringify({ ...localStorage }).includes(k) || JSON.stringify({ ...sessionStorage }).includes(k) || document.cookie.includes(k), KEY); check("own: the key goes to the visitor's endpoint alone, in its Authorization header, and is stored nowhere", call.auth === `Bearer ${KEY}` && withKey.length === 1 && withKey[0] === endpoint && !stored, `${requests.length} requests; the key in ${withKey.length}`); p.dump(); model.close(); if (!shared) await srv.close(); } // The visitor's coding agent: `wai mcp --toolset app` on this machine, driven over its stdio as // an agent drives it; the page connects to its bridge with the pairing code, and the agent // streams the show into it, then exports it as `wai create --from` does. async function scenarioAgent() { const dir = path.join(OUT, "agent"); fs.mkdirSync(dir, { recursive: true }); const mcp = spawn(WAI, ["mcp", "--toolset", "app", "--project", dir], { stdio: ["pipe", "pipe", "pipe"] }); procs.push(mcp); let errText = ""; mcp.stderr.on("data", (d) => { errText += d; }); const first = await until("the MCP server names its bridge", 10000, () => errText.match(/http:\/\/127\.0\.0\.1:(\d+), pairing code (\w+)/)); const [, port, code] = first; let buf = ""; const waiting = []; mcp.stdout.on("data", (d) => { buf += d; let nl; while ((nl = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, nl); buf = buf.slice(nl + 1); const w = waiting.shift(); if (w) w(JSON.parse(line)); } }); let id = 0; const rpc = (method, params) => new Promise((ok) => { waiting.push(ok); mcp.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }) + "\n"); }); await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "e2e", version: "0" } }); const srv = shared ? shared.srv : await serveSameOrigin(ROOT, { service: "http://127.0.0.1:9", relayPort: 9 }); const p = await page(`${srv.url}${CREATE}`, "create-agent"); await until("Create loads", STEP, () => p.evaluate(() => !!window.createTest)); await p.check('input[name="source"][value="agent"]'); await p.fill("#agent-bridge", `http://127.0.0.1:${port}`); await p.fill("#agent-code", code); await p.click("#build"); await until("the page is connected to the bridge", STEP, () => p.evaluate(() => /Connected/.test(document.getElementById("agent-state").textContent)), 200); const r = await rpc("tools/call", { name: "app_stream", arguments: { program: SHOW_TEXT, chunk: 12, pace_ms: 10 } }); check("agent: the agent's stream reaches the connected page", r.result && !r.result.isError && r.result.structuredContent.delivered === 1, JSON.stringify(r.result && r.result.structuredContent).slice(0, 120)); await until("the page renders the agent's app", STEP, () => p.evaluate(() => window.createTest.done >= 1)); const fin = await p.evaluate(() => window.createTest.last); const counts = await p.evaluate(() => window.createTest.snapshots.map((x) => x.widgets)); check("agent: the page renders the app as the agent streams it", fin.errors.length === 0 && fin.nodes.length === 8 && new Set(counts).size >= 5 && counts.every((c, i) => !i || c >= counts[i - 1]), `${counts.length} snapshots, ${fin.nodes.length} widgets`); const wrong = await p.evaluate(async (u) => (await fetch(u)).status, `http://127.0.0.1:${port}/create/stream?code=wrong`).catch((e) => String(e)); check("agent: the bridge refuses a page without the pairing code", wrong === 403 || /Failed to fetch/.test(String(wrong)), String(wrong)); const e = await rpc("tools/call", { name: "app_export", arguments: { program: SHOW_TEXT, dir: "agent-export", runtime_dir: RT } }); const ex = path.join(dir, "agent-export"); const checked = wai(["app", "check"], ex); check("agent: app_export writes the project `wai create --from` writes, and its check passes", e.result && !e.result.isError && fs.existsSync(path.join(ex, "wai.json")) && checked.code === 0, JSON.stringify(e.result && e.result.structuredContent).slice(0, 160)); p.dump(); mcp.stdin.end(); if (!shared) await srv.close(); } try { await launch(); for (const [name, f] of [["create", scenarioCreate], ["built", scenarioBuilt], ["refused", scenarioRefused], ["own", scenarioOwn], ["agent", scenarioAgent]]) { if (!ONLY.includes(name)) continue; console.log(`== ${name}`); try { await f(); } catch (e) { check(`${name}: ran to the end`, false, (e && e.stack) || String(e)); } } if (shared) shared.p.dump(); } finally { if (browser) await browser.close().catch(() => {}); for (const p of procs) if (p.exitCode == null) p.kill("SIGTERM"); for (const p of procs) if (p.exitCode == null) p.kill("SIGTERM"); killStrays(); } fs.writeFileSync(path.join(OUT, "results.json"), JSON.stringify({ results, failed }, null, 1)); console.log(`create e2e: ${results.length - failed} passed, ${failed} failed`); process.exit(failed ? 1 : 0);