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) }