From b8a595db054fc8cd4bac1f525532d94bbdb605ba Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sun, 5 Jul 2026 01:11:29 +0300 Subject: [PATCH] test(ai-autopilot): add a headed demo mode to the WebContainer harness HEADED=1 opens a real Chrome window, runs the checks, and renders the live app served by the WebContainer (loads the preview URL in an iframe on the booting page, which owns the service worker), then stays open until Ctrl-C. The default headless verification path is unchanged (still 15/15). Harness-only, not published. --- .../harness/webcontainer/README.md | 6 +++- .../harness/webcontainer/drive.mjs | 23 +++++++++----- .../harness/webcontainer/index.html | 31 ++++++++++++++----- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/ai-autopilot/harness/webcontainer/README.md b/packages/ai-autopilot/harness/webcontainer/README.md index c7a9c69..a8ad7fe 100644 --- a/packages/ai-autopilot/harness/webcontainer/README.md +++ b/packages/ai-autopilot/harness/webcontainer/README.md @@ -23,7 +23,11 @@ Chromium** against the compiled adapter. This directory is that proof. ```bash pnpm build # compile the adapter into dist/ -node harness/webcontainer/drive.mjs # boot-and-serve proof; exits non-zero on any failed check +node harness/webcontainer/drive.mjs # headless boot-and-serve proof; exits non-zero on any failed check + +HEADED=1 node harness/webcontainer/drive.mjs # opens a real Chrome window, runs the checks, + # and renders the live app served by the WebContainer; + # stays open until Ctrl-C ``` Requirements: diff --git a/packages/ai-autopilot/harness/webcontainer/drive.mjs b/packages/ai-autopilot/harness/webcontainer/drive.mjs index f7a8bf6..a8b2c3e 100644 --- a/packages/ai-autopilot/harness/webcontainer/drive.mjs +++ b/packages/ai-autopilot/harness/webcontainer/drive.mjs @@ -4,19 +4,22 @@ // // Prereqs: build the package (`pnpm build`) and have Google Chrome installed, or // a Playwright Chromium (`npx playwright install chromium`). Then: -// node harness/webcontainer/drive.mjs +// node harness/webcontainer/drive.mjs # headless verification (exits 0/1) +// HEADED=1 node harness/webcontainer/drive.mjs # opens a real window + live preview, stays open // // Needs network: WebContainer downloads its runtime from StackBlitz on first boot. import { chromium } from 'playwright-core' import { startServer } from './server.mjs' +const headed = process.env.HEADED === '1' + async function launch() { // Prefer a system Chrome (no browser download); fall back to a Playwright Chromium. try { - return await chromium.launch({ headless: true, channel: 'chrome' }) + return await chromium.launch({ headless: !headed, channel: 'chrome' }) } catch { try { - return await chromium.launch({ headless: true }) + return await chromium.launch({ headless: !headed }) } catch (err) { console.error( 'No usable Chromium. Install Google Chrome, or run `npx playwright install chromium`.\n' + err, @@ -34,18 +37,24 @@ page.on('pageerror', (e) => console.log('[pageerror]', e.message)) let result try { - await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'load' }) + await page.goto(`http://127.0.0.1:${port}/${headed ? '?demo=1' : ''}`, { waitUntil: 'load' }) await page.waitForFunction(() => window.__RESULT__ && window.__RESULT__.done, { timeout: 90000 }) result = await page.evaluate(() => window.__RESULT__) } catch (e) { result = { done: false, ok: false, error: 'harness timeout: ' + e.message } } -await browser.close() -server.close() - const passed = (result.checks ?? []).filter((c) => c.ok).length const total = (result.checks ?? []).length console.log(`\n${result.ok ? 'OK' : 'FAILED'} — ${passed}/${total} checks passed`) if (result.error) console.log('error:', result.error) + +if (headed) { + // Leave the window (and the WebContainer's live preview) open to inspect. + console.log('\nBrowser window is open with the live preview. Press Ctrl-C to exit.') + await new Promise(() => {}) +} + +await browser.close() +server.close() process.exit(result.ok ? 0 : 1) diff --git a/packages/ai-autopilot/harness/webcontainer/index.html b/packages/ai-autopilot/harness/webcontainer/index.html index 1a96d23..3bee484 100644 --- a/packages/ai-autopilot/harness/webcontainer/index.html +++ b/packages/ai-autopilot/harness/webcontainer/index.html @@ -60,16 +60,31 @@ const served = await s.exec("node -e \"fetch('http://localhost:3000').then(r=>r.text()).then(t=>process.stdout.write(t))\"") check('the started server actually serves', served.stdout.includes('hello from webcontainer'), JSON.stringify(served.stdout.trim())) - await dev.stop() - await s.dispose() - check('dispose blocks further exec', await (async () => { try { await s.exec('true'); return false } catch { return true } })()) + // `?demo=1`: leave the server up and render the live preview, so a headed + // browser shows the app actually served by the WebContainer. + if (new URLSearchParams(location.search).has('demo')) { + log('demo mode: server left running; live preview below') + const h = document.createElement('h3') + h.textContent = 'Live app served by the WebContainer:' + const a = document.createElement('a') + a.href = preview.url; a.textContent = preview.url; a.target = '_blank' + const frame = document.createElement('iframe') + frame.src = preview.url + frame.style.cssText = 'display:block;width:100%;height:120px;border:1px solid #888;margin-top:8px' + document.body.append(h, a, frame) + window.__RESULT__ = { done: true, ok: checks.every(c => c.ok), checks, demo: true } + } else { + await dev.stop() + await s.dispose() + check('dispose blocks further exec', await (async () => { try { await s.exec('true'); return false } catch { return true } })()) - // Single-instance guard: a fresh boot after dispose must succeed. - const s2 = await runner.boot() - check('boot succeeds again after dispose (single-instance slot freed)', !!s2.id) - await s2.dispose() + // Single-instance guard: a fresh boot after dispose must succeed. + const s2 = await runner.boot() + check('boot succeeds again after dispose (single-instance slot freed)', !!s2.id) + await s2.dispose() - window.__RESULT__ = { done: true, ok: checks.every(c => c.ok), checks } + window.__RESULT__ = { done: true, ok: checks.every(c => c.ok), checks } + } } catch (err) { log('ERROR ' + (err && err.stack || err)) window.__RESULT__ = { done: true, ok: false, checks, error: String(err && err.message || err) }