// A2 sub-agent: 5-cycle Playwright loop covering UI + backend triggers. // All triggers use auction_mode='mock' to avoid auction LLM calls; // translation/eval phases still call Haiku (cheapest tier). // // Outputs: // outputs/playwright_loop_findings.md (running journal) // outputs/loop_screenshots/cycle_N_step_M.png /* eslint-disable no-console */ const fs = require("fs"); const path = require("path"); const http = require("http"); const { chromium } = require("playwright"); const BASE_UI = process.env.BASE_UI || "http://127.0.0.1:3001"; const BASE_API = process.env.BASE_API || "http://127.0.0.1:8000"; const OUT_DIR = path.resolve(__dirname, "..", "..", "outputs"); const SHOT_DIR = path.join(OUT_DIR, "loop_screenshots"); const FINDINGS = path.join(OUT_DIR, "playwright_loop_findings.md"); fs.mkdirSync(SHOT_DIR, { recursive: true }); function ts() { return new Date().toISOString(); } function append(line) { fs.appendFileSync(FINDINGS, line + "\n"); } function postJson(url, body) { return new Promise((resolve, reject) => { const data = JSON.stringify(body); const u = new URL(url); const req = http.request( { hostname: u.hostname, port: u.port, path: u.pathname, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), }, }, (res) => { let chunks = ""; res.on("data", (c) => (chunks += c)); res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(chunks) }); } catch (e) { resolve({ status: res.statusCode, body: chunks }); } }); }, ); req.on("error", reject); req.write(data); req.end(); }); } function getJson(url) { return new Promise((resolve, reject) => { http .get(url, (res) => { let chunks = ""; res.on("data", (c) => (chunks += c)); res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(chunks) }); } catch (e) { resolve({ status: res.statusCode, body: chunks }); } }); }) .on("error", reject); }); } async function waitForTerminal(eventId, timeoutMs = 150000) { const t0 = Date.now(); const terminal = new Set([ "COMMITTED", "SUBMITTED", "FAILED", "REJECTED", ]); while (Date.now() - t0 < timeoutMs) { const r = await getJson(`${BASE_API}/events/${eventId}`); if (r.body && terminal.has(r.body.status)) { return r.body; } await new Promise((r) => setTimeout(r, 2500)); } return null; } const SESSION_TAG = `a2-${Date.now()}`; const CYCLES = [ { label: "3-mock-bids", trigger: { event_source: "user_payload", title: `Will event A1 happen by 2026-12-31? [${SESSION_TAG}-c1]`, sources: [{ name: "test-c1", url: `https://test/c1?s=${SESSION_TAG}` }], language: "en", auction_mode: "mock", mock_bids: [ { agent_address: "0xagent_a", bid_amount: 0.5, stake_amount: 5.0, reputation: 0.9, }, { agent_address: "0xagent_b", bid_amount: 0.7, stake_amount: 5.0, reputation: 0.8, }, { agent_address: "0xagent_c", bid_amount: 0.45, stake_amount: 5.0, reputation: 0.85, }, ], }, }, { label: "1-mock-bid", trigger: { event_source: "user_payload", title: `Will event B2 happen by 2026-12-31? [${SESSION_TAG}-c2]`, sources: [{ name: "test-c2", url: `https://test/c2?s=${SESSION_TAG}` }], language: "en", auction_mode: "mock", mock_bids: [ { agent_address: "0xagent_solo", bid_amount: 0.6, stake_amount: 5.0, reputation: 0.95, }, ], }, }, { label: "0-mock-bids-edge", trigger: { event_source: "user_payload", title: `Will event C3 happen by 2026-12-31? [${SESSION_TAG}-c3]`, sources: [{ name: "test-c3", url: `https://test/c3?s=${SESSION_TAG}` }], language: "en", auction_mode: "mock", mock_bids: [], }, }, { label: "rep-gate-high-vs-low", trigger: { event_source: "user_payload", title: `Will event D4 happen by 2026-12-31? [${SESSION_TAG}-c4]`, sources: [{ name: "test-c4", url: `https://test/c4?s=${SESSION_TAG}` }], language: "en", auction_mode: "mock", mock_bids: [ { agent_address: "0xagent_high", bid_amount: 0.4, stake_amount: 5.0, reputation: 0.99, }, { agent_address: "0xagent_low1", bid_amount: 0.55, stake_amount: 5.0, reputation: 0.1, }, { agent_address: "0xagent_low2", bid_amount: 0.6, stake_amount: 5.0, reputation: 0.15, }, ], }, }, { label: "explore-other-pages", trigger: { event_source: "user_payload", title: `Will event E5 happen by 2026-12-31? [${SESSION_TAG}-c5]`, sources: [{ name: "test-c5", url: `https://test/c5?s=${SESSION_TAG}` }], language: "en", auction_mode: "mock", mock_bids: [ { agent_address: "0xagent_a", bid_amount: 0.5, stake_amount: 5.0, reputation: 0.9, }, { agent_address: "0xagent_b", bid_amount: 0.7, stake_amount: 5.0, reputation: 0.8, }, ], }, }, ]; async function runCycle(browser, cycleN, spec) { const cycleStart = ts(); const findings = []; findings.push(`\n## Cycle ${cycleN}: ${spec.label} (${cycleStart})`); console.log(`\n=== Cycle ${cycleN}: ${spec.label} ===`); // 1. Trigger let eventId = null; let triggerResp = null; try { triggerResp = await postJson(`${BASE_API}/trigger/event`, spec.trigger); findings.push( `- Trigger HTTP ${triggerResp.status}: \`${JSON.stringify(triggerResp.body).slice(0, 200)}\``, ); eventId = triggerResp.body && triggerResp.body.event_id; } catch (e) { findings.push(`- Trigger FAILED with exception: ${e.message}`); } if (cycleN === 3) { // 0-bid edge: we expect this to either fail at validation OR succeed via fallback. findings.push(`- Edge case: 0 mock bids — recording behavior.`); } let finalEvent = null; if (eventId) { finalEvent = await waitForTerminal(eventId); if (finalEvent) { findings.push( `- Lifecycle terminal status: **${finalEvent.status}**, winner=${finalEvent.winner_address || "n/a"}, winning_bid=${finalEvent.winning_bid ?? "n/a"}`, ); } else { findings.push(`- Lifecycle did NOT reach terminal within 90s.`); } } // 2. Playwright UI walk const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, }); const page = await context.newPage(); const errors = []; page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`)); page.on("console", (msg) => { if (msg.type() === "error") errors.push(`console.error: ${msg.text()}`); }); let step = 0; const shot = async (label) => { step += 1; const p = path.join(SHOT_DIR, `cycle_${cycleN}_step_${step}_${label}.png`); try { await page.screenshot({ path: p, fullPage: false }); } catch (e) { findings.push(`- Screenshot failed for ${label}: ${e.message}`); } }; try { // /events list page await page.goto(`${BASE_UI}/events`, { waitUntil: "domcontentloaded", timeout: 20000, }); await page.waitForTimeout(1500); await shot("events_list"); if (eventId) { await page.goto(`${BASE_UI}/events/${eventId}`, { waitUntil: "domcontentloaded", timeout: 20000, }); await page.waitForTimeout(2500); await shot("event_detail"); // Try click any DAG node (visible elements only) const dagNodes = await page.$$('[data-testid^="dag-node"], .dag-node, svg g[data-id]'); findings.push(`- Found ${dagNodes.length} DAG-ish nodes on /events/${eventId}.`); let dagClickedAny = false; for (const n of dagNodes.slice(0, 5)) { try { const vis = await n.isVisible(); if (!vis) continue; await n.click({ timeout: 2000 }); dagClickedAny = true; await page.waitForTimeout(500); await shot("dag_click"); break; } catch (e) { /* try next */ } } findings.push(`- DAG click reached visible node: ${dagClickedAny}`); // Check Timeline test ID & sub-phase chips const tl = await page.$('[data-testid*="timeline"], .timeline, [class*="Timeline"]'); const subPhaseChips = await page.$('[data-testid="sub-phase-chips"]'); const debatePanel = await page.$('[data-testid="agent-debate-panel"], [data-testid="agent-debate-panel-empty"]'); findings.push(`- Timeline element present: ${tl ? "yes" : "no"}`); findings.push(`- sub-phase-chips present: ${subPhaseChips ? "yes" : "no"}`); findings.push(`- agent-debate-panel present: ${debatePanel ? "yes" : "no"}`); // Try clicking any tab/button to exercise interactivity const buttons = await page.$$('button[role="tab"], [role="tab"]'); findings.push(`- Tabs found: ${buttons.length}`); if (buttons.length > 1) { try { await buttons[1].click({ timeout: 2000 }); await page.waitForTimeout(500); await shot("tab_click"); } catch (e) { findings.push(`- Tab click failed: ${e.message}`); } } // Probe DOM for status text matching our final event if (finalEvent) { const html = await page.content(); const seen = html.includes(finalEvent.status); findings.push(`- Final status \`${finalEvent.status}\` visible in DOM: ${seen}`); // Cross-check phases page const phasesResp = await getJson( `${BASE_API}/events/${eventId}/phases`, ); if (phasesResp.body && Array.isArray(phasesResp.body)) { const completed = phasesResp.body.filter((p) => p.status === "completed").length; const failed = phasesResp.body.filter((p) => p.status === "failed").length; findings.push( `- /phases API: ${phasesResp.body.length} total, ${completed} completed, ${failed} failed`, ); } } } // Cycle 5: explore other pages if (cycleN === 5) { const otherRoutes = ["/", "/leaderboard", "/about", "/operators"]; for (const r of otherRoutes) { try { await page.goto(`${BASE_UI}${r}`, { waitUntil: "domcontentloaded", timeout: 15000, }); await page.waitForTimeout(1200); await shot(`route_${r.replace(/\//g, "_") || "root"}`); findings.push(`- ${r}: loaded OK`); } catch (e) { findings.push(`- ${r}: FAILED ${e.message}`); } } } } catch (e) { findings.push(`- Playwright walk error: ${e.message}`); } if (errors.length > 0) { findings.push(`- JS errors observed (${errors.length}):`); for (const er of errors.slice(0, 5)) findings.push(` - ${er}`); } else { findings.push(`- JS errors observed: 0`); } await context.close(); findings.push(`- Cycle finished at ${ts()}`); for (const f of findings) append(f); return { eventId, finalEvent, errors: errors.length }; } (async () => { if (!fs.existsSync(FINDINGS)) { append(`# Playwright Loop Findings (A2 sub-agent)`); append(`Started ${ts()}`); } else { append(`\n---`); append(`# A2 Loop Session ${ts()}`); } const browser = await chromium.launch({ headless: true }); const results = []; for (let i = 0; i < CYCLES.length; i++) { try { const r = await runCycle(browser, i + 1, CYCLES[i]); results.push(r); } catch (e) { append(`- Cycle ${i + 1} threw: ${e.message}`); results.push({ error: e.message }); } } await browser.close(); append(`\n## Session summary`); append(`- Cycles attempted: ${CYCLES.length}`); append( `- Cycles completed: ${results.filter((r) => !r.error).length}`, ); append(`- Session end: ${ts()}`); console.log("DONE"); console.log(JSON.stringify(results, null, 2)); })();