import { chromium } from 'playwright'; import fs from 'node:fs/promises'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import path from 'node:path'; // Playwright 1.58 disables ThirdPartyStoragePartitioning by default. Restore // Chromium's normal partitioning for a representative cross-site iframe test. const require = createRequire(import.meta.url); const { chromiumSwitches } = require(path.join(path.dirname(require.resolve('playwright-core/package.json')), 'lib/server/chromium/chromiumSwitches.js')); const disabledFeatures = chromiumSwitches(false).find(arg => arg.startsWith('--disable-features=')); const url = 'https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi'; const label = process.argv[2] ?? 'agent-space-embedded'; const firstMessage = process.argv.includes('--first-message'); const result = { date: new Date().toISOString(), url, firstMessage }; const context = await chromium.launchPersistentContext('../validation/agent-browser-profile/space', { executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1080 }, ignoreDefaultArgs: [disabledFeatures], args: [disabledFeatures.replace('ThirdPartyStoragePartitioning,', ''), '--enable-unsafe-webgpu', '--use-angle=vulkan', '--enable-features=Vulkan', '--disable-vulkan-surface'], }); context.setDefaultTimeout(240_000); try { const page = await context.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded' }); console.log('Hugging Face page opened'); await page.waitForFunction(() => [...document.querySelectorAll('iframe')].some(frame => frame.src.includes('minicpm5-2b-webgpu-pi'))); // Hugging Face can replace its initial iframe while the parent initializes. let app; for (let attempt = 0; attempt < 4; attempt++) { const element = await page.waitForSelector('iframe[src*="minicpm5-2b-webgpu-pi"]'); app = await element.contentFrame(); assert(app, 'The deployed Hugging Face app frame must exist.'); try { await app.waitForFunction(() => window.browserPi?.terminal); await app.evaluate(() => window.browserPi.ready); break; } catch (error) { if (attempt === 3 || !/detached|context was destroyed/i.test(error.message)) throw error; } } result.frameUrl = app.url(); result.cachedBeforeSend = await app.evaluate(() => window.browserPi.rpc('cache_status')); console.log('App initialized'); const task = 'Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.'; if (firstMessage) { await app.evaluate(() => document.querySelector('#new-chat').onclick()); await app.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/report.md' })); // Activate the iframe with a real click before sending keyboard events. await app.locator('#pi-terminal').click(); await app.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(task); await app.waitForFunction(text => window.browserPi.terminal.text === text, task, { timeout: 15_000 }); console.log('First message typed'); await app.locator('#send').click(); if (!result.cachedBeforeSend) { await app.waitForFunction(() => document.querySelector('#model-dialog').open); console.log('First-message modal opened'); assert.equal(await app.locator('#model-preview').textContent(), task); await page.screenshot({ path: `../validation/${label}-modal.png`, fullPage: true }); } } const start = Date.now(); if (!firstMessage || !result.cachedBeforeSend) await app.locator(firstMessage ? '#model-confirm' : '#load').click(); await app.waitForFunction(() => window.browserPi.loaded || (!window.browserPi.busy && (document.querySelector('#notice').textContent || !document.querySelector('#model-error').hidden))); assert.equal(await app.evaluate(() => window.browserPi.loaded), true, await app.locator('#load-status').textContent()); result.loadMs = Date.now() - start; result.device = await app.evaluate(() => window.browserPi.diagnostics.device); if (!firstMessage) { await app.waitForFunction(() => !window.browserPi.busy); // Await the async UI handler: checking !busy immediately after a click can // precede the worker's busy event and race the next setup command. await app.evaluate(() => document.querySelector('#new-chat').onclick()); await app.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/report.md' })); await app.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(task); await app.locator('#send').click(); await app.waitForFunction(() => window.browserPi.busy); await app.waitForFunction(() => !window.browserPi.busy); } else { await app.waitForFunction(() => window.browserPi.diagnostics.events.includes('agent_end') && !window.browserPi.busy); assert.equal(await app.locator('#model-dialog').evaluate(el => el.open), false); } const state = await app.evaluate(() => window.browserPi.rpc('snapshot')); result.messages = state.messages; result.report = state.files['/workspace/report.md']; result.terminalScreen = await app.evaluate(() => window.browserPi.terminal.readScreen()); assert.equal(state.messages.filter(m => m.role === 'user').length, 1); assert.match(result.report ?? '', /144/); assert(state.messages.some(m => m.role === 'toolResult' && m.toolName === 'bash' && !m.isError)); assert(state.messages.filter(m => m.role === 'assistant').at(-1).content.some(c => c.type === 'text' && c.text.includes('144'))); await page.screenshot({ path: `../validation/${label}.png`, fullPage: true }); result.passed = true; console.log('Embedded Hugging Face Space: real UI → Pi → WebGPU → bash → verified report passed.'); } catch (error) { result.failure = error.stack; throw error; } finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await context.close(); }