// Records the full "OAuth → MCP → call" flow as one dark-themed mp4: // 1) client title card (mcp add + pick OAuth) // 2) REAL browser consent UI (login → connected), forced dark via colorScheme // 3) chat replay of the real tool call // Run from testkit/ (playwright resolves here): node record-flow.mjs import { chromium } from "playwright"; import { execSync } from "node:child_process"; import { createRuntime, cookieConsentStrategy } from "../vendor/mcporter/dist/index.js"; import { renderPlayer } from "./src/render.mjs"; import { mkdtempSync, writeFileSync, readdirSync, renameSync, rmSync, mkdirSync } from "node:fs"; import { tmpdir, homedir } from "node:os"; import { join } from "node:path"; // fresh OAuth so phase 2 records the real browser consent (not a cached token) rmSync(join(homedir(), ".mcporter"), { recursive: true, force: true }); const W = 1000, H = 720, APP = process.env.MCP_APP ?? "http://localhost:5173"; const EMAIL = process.env.MCP_EMAIL ?? "admin@demo.test", PW = process.env.MCP_PASSWORD ?? "demo-password-12345"; const TMP = "/tmp/rec-flow"; rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true }); const newRt = () => { const d = mkdtempSync(join(tmpdir(), "rf-")); writeFileSync(join(d, "mcporter.json"), JSON.stringify({ mcpServers: { selfhost: { url: APP + "/mcp" } } })); return createRuntime({ configPath: join(d, "mcporter.json") }); }; const recCtx = (b, dir) => b.newContext({ viewport: { width: W, height: H }, colorScheme: "dark", recordVideo: { dir, size: { width: W, height: H } } }); const grab = (dir, name) => { const f = readdirSync(dir).find(x => x.endsWith(".webm")); renameSync(join(dir, f), `${TMP}/${name}.webm`); }; { const html = `
`; writeFileSync(`${TMP}/title.html`, html); const b = await chromium.launch(); const ctx = await recCtx(b, `${TMP}/v1`); const p = await ctx.newPage(); await p.goto(`file://${TMP}/title.html`); await p.waitForFunction("window.__done===true", { timeout: 15000 }).catch(()=>{}); await p.waitForTimeout(600); await ctx.close(); await b.close(); grab(`${TMP}/v1`, "1"); console.log("phase1 ✓"); } { let req; const rt = await newRt(); try { await rt.listTools("selfhost", { autoAuthorize: true, oauthSessionOptions: { consentStrategy: async r => { req = r; throw new Error("x"); } } }); } catch {} const b = await chromium.launch(); const ctx = await recCtx(b, `${TMP}/v2`); const p = await ctx.newPage(); await p.goto(req.authorizationUrl, { waitUntil: "domcontentloaded" }); await p.waitForSelector("input[type=email]", { timeout: 10000 }); await p.waitForTimeout(700); await p.type("input[type=email]", EMAIL, { delay: 45 }); await p.waitForTimeout(250); await p.type("input[type=password]", PW, { delay: 28 }); await p.waitForTimeout(450); await p.click("button[type=submit]"); await p.waitForTimeout(2600); await p.setContent('🔓 Connected to MCP server'); await p.waitForTimeout(1700); await ctx.close(); await b.close(); grab(`${TMP}/v2`, "2"); console.log("phase2 ✓ (dark login)"); } { const rt = await newRt(); const opts = { autoAuthorize: true, oauthSessionOptions: { consentStrategy: cookieConsentStrategy({ appBaseUrl: APP, email: EMAIL, password: PW }) } }; const tools = await rt.listTools("selfhost", opts); const res = await rt.callTool("selfhost", "execute", { args: { code: "return 6 * 7;" }, ...opts }); const text = res.content.filter(c=>c.type==="text").map(c=>c.text).join(""); const run = { task: "Run 6 × 7 in the sandbox using the execute tool", brain: "scripted", meta: { server: APP + "/mcp" }, ok: text==="42", turns: [ { role: "user", text: "Run 6 × 7 in the sandbox using the execute tool" }, { role: "assistant", kind: "reasoning", text: "Connected to the MCP server. I'll call execute with a tiny program." }, { role: "tool", call: { name: "tools/list", args: {} }, result: tools.map(t=>t.name), ok: true, text: tools.map(t=>t.name).join(", ") }, { role: "tool", call: { name: "execute", args: { code: "return 6 * 7;" } }, result: res.content, ok: true, text }, { role: "assert", kind: "toBe", actual: text, expected: "42", ok: text === "42" } ]}; writeFileSync(`${TMP}/chat.html`, renderPlayer(run)); const b = await chromium.launch(); const ctx = await recCtx(b, `${TMP}/v3`); const p = await ctx.newPage(); await p.goto(`file://${TMP}/chat.html`); await p.waitForFunction("window.__done===true", { timeout: 30000 }).catch(()=>{}); await p.waitForTimeout(1000); await ctx.close(); await b.close(); grab(`${TMP}/v3`, "3"); console.log("phase3 ✓ →", text); } for (const n of ["1", "2", "3"]) execSync(`ffmpeg -y -i ${TMP}/${n}.webm -vf "scale=${W}:${H}:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2:color=0x0b0f17,fps=30" -pix_fmt yuv420p -c:v libx264 -preset veryfast ${TMP}/${n}.mp4 2>/dev/null`); writeFileSync(`${TMP}/list.txt`, ["1","2","3"].map(n=>`file '${TMP}/${n}.mp4'`).join("\n")); execSync(`ffmpeg -y -f concat -safe 0 -i ${TMP}/list.txt -c copy ${import.meta.dirname}/runs/oauth-flow.mp4 2>/dev/null`); console.log("✓ runs/oauth-flow.mp4");