From c97d36bbb28a2659be6ba2fed4d7601b2876ecfc Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Fri, 30 Oct 2026 23:03:27 +0800 Subject: [PATCH] Add isolated browser diagnostics using existing review images --- .../creative-browser-diagnostics.yml | 21 ++++++++++++++++++ .../tests/canvas-ui-integration.ts | 22 ++++++++++++++----- compose.creative-diagnostics.yml | 5 +++++ scripts/integration.sh | 1 + 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 .gitea/workflows/creative-browser-diagnostics.yml create mode 100644 compose.creative-diagnostics.yml diff --git a/.gitea/workflows/creative-browser-diagnostics.yml b/.gitea/workflows/creative-browser-diagnostics.yml new file mode 100644 index 0000000..0533b3a --- /dev/null +++ b/.gitea/workflows/creative-browser-diagnostics.yml @@ -0,0 +1,21 @@ +name: creative browser diagnosis +on: + push: + branches: [codex/creative-browser-diagnosis] +jobs: + diagnose: + runs-on: ces-control + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Require the exact images built by review run 595 + run: | + docker image inspect "$STUDIO_REVIEW_IMAGE" "$WATERFALL_WIKI_IMAGE" "$WATERFALL_WIKI_TEST_IMAGE" --format '{{.Id}}' + - name: Diagnose existing images in disposable PostgreSQL and S3 stack + run: bash scripts/integration.sh + env: + STUDIO_REVIEW_IMAGE: creative-studio-review:cdcd4c4b94d445829d624800193c66955ff09704 + WATERFALL_WIKI_IMAGE: creative-wiki-review:cdcd4c4b94d445829d624800193c66955ff09704 + WATERFALL_WIKI_TEST_IMAGE: creative-wiki-tests:cdcd4c4b94d445829d624800193c66955ff09704 + CREATIVE_BROWSER_DIAGNOSTICS: '1' + GITEA_RUN_ID: ${{ gitea.run_id }} diff --git a/apps/waterfall-wiki/tests/canvas-ui-integration.ts b/apps/waterfall-wiki/tests/canvas-ui-integration.ts index bbfff4a..ac393ca 100644 --- a/apps/waterfall-wiki/tests/canvas-ui-integration.ts +++ b/apps/waterfall-wiki/tests/canvas-ui-integration.ts @@ -1,5 +1,7 @@ import assert from 'node:assert/strict'; -import {randomUUID} from 'node:crypto'; +import {randomUUID,createHash} from 'node:crypto'; +import {createRequire} from 'node:module'; +import {availableParallelism,loadavg,freemem,totalmem} from 'node:os'; import {chromium,type Request} from 'playwright-core'; import {db} from '../src/lib/db'; import {hashPassword,ACCOUNT_COOKIE} from '../src/lib/account-crypto'; @@ -8,18 +10,28 @@ export async function verifyCanvasAgentUI(base:string){ assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1'); const owner=randomUUID(),email=owner+'@example.com',password='Canvas integration example1!'; await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,$3,now())",[owner,email,await hashPassword(password)]); + const probe=async(url:string)=>{ + const started=Date.now(); + try{const response=await fetch(url,{signal:AbortSignal.timeout(5000)});const body=await response.text();return {status:response.status,type:response.headers.get('content-type'),bytes:Buffer.byteLength(body),sha256:createHash('sha256').update(body).digest('hex'),ms:Date.now()-started}} + catch(error){return {error:error instanceof Error?error.message:String(error),ms:Date.now()-started}} + }; + const html=await fetch(base+'/infinite_canvas',{signal:AbortSignal.timeout(10000)}).then(r=>r.text()); + const scripts=[...html.matchAll(/]+src="([^"?]+)[^">]*"/g)].map(match=>match[1]); + console.log('Canvas script proxy comparison',JSON.stringify(await Promise.all(scripts.map(async path=>({path,proxy:await probe(base+path),direct:await probe('http://prompt-studio:3000'+path)}))))); const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); + console.log('Canvas browser environment',JSON.stringify({browser:browser.version(),playwright:createRequire(import.meta.url)('playwright-core/package.json').version,node:process.version,cpu:availableParallelism(),load:loadavg(),freeMemory:freemem(),totalMemory:totalmem()})); const context=await browser.newContext({viewport:{width:1440,height:1000}}),errors:string[]=[]; const page=await context.newPage(),canvasRequests:string[]=[],pending=new Set(),network:string[]=[]; + const resources:{ms:number;event:string;path:string;status?:number;type?:string;duration?:number}[]=[],requestStarts=new Map(); const started=Date.now(),navigation:{ms:number;event:string;path:string}[]=[]; const mark=(event:string)=>{navigation.push({ms:Date.now()-started,event,path:new URL(page.url()).pathname});if(navigation.length>60)navigation.shift()}; page.on('framenavigated',frame=>{if(frame===page.mainFrame())mark('framenavigated')}); page.on('domcontentloaded',()=>mark('domcontentloaded'));page.on('load',()=>mark('load')); const requestName=(request:{method:()=>string;url:()=>string})=>request.method()+' '+new URL(request.url()).pathname; - page.on('request',request=>pending.add(request)); + page.on('request',request=>{pending.add(request);requestStarts.set(request,Date.now())}); page.on('requestfinished',request=>pending.delete(request)); page.on('requestfailed',request=>{pending.delete(request);network.push(requestName(request)+': '+request.failure()?.errorText)}); - page.on('response',response=>{const url=new URL(response.url());if(url.pathname.startsWith('/api/canvases'))canvasRequests.push(`${response.request().method()} ${url.pathname} ${response.status()}`)}); + page.on('response',response=>{const url=new URL(response.url());if(url.pathname.includes('/_next/')||url.pathname==='/api/account/session'){resources.push({ms:Date.now()-started,event:'response',path:url.pathname,status:response.status(),type:response.headers()['content-type'],duration:Date.now()-(requestStarts.get(response.request())||Date.now())});if(resources.length>100)resources.shift()}if(url.pathname.startsWith('/api/canvases'))canvasRequests.push(`${response.request().method()} ${url.pathname} ${response.status()}`)}); try{ page.setDefaultTimeout(20000);page.on('pageerror',e=>errors.push(e.message)); mark('open landing');await page.goto(base+'/',{waitUntil:'domcontentloaded'}); @@ -110,8 +122,8 @@ export async function verifyCanvasAgentUI(base:string){ }catch(error){ const statuses=await page.getByRole('status').allTextContents().catch(()=>[]); const headings=await page.getByRole('heading').allTextContents().catch(()=>[]); - const documentState=await page.evaluate(()=>({readyState:document.readyState,navigation:performance.getEntriesByType('navigation').map(entry=>{const timing=entry as PerformanceNavigationTiming;return {type:timing.type,domContentLoadedEventEnd:timing.domContentLoadedEventEnd,loadEventEnd:timing.loadEventEnd}})})).catch(()=>null); - console.error('Canvas agent UI diagnostics',JSON.stringify({url:page.url(),browserConnected:browser.isConnected(),headings,statuses,documentState,navigation,pending:[...pending].map(requestName),network:network.slice(-20),canvasRequests:canvasRequests.slice(-20),errors})); + const documentState=await page.evaluate(()=>({readyState:document.readyState,navigation:performance.getEntriesByType('navigation').map(entry=>{const timing=entry as PerformanceNavigationTiming;return {type:timing.type,domContentLoadedEventEnd:timing.domContentLoadedEventEnd,loadEventEnd:timing.loadEventEnd}}),nextData:(()=>{const element=document.getElementById('__NEXT_DATA__');const data=element?JSON.parse(element.textContent||'{}'):{};return {page:data.page,buildId:data.buildId,assetPrefix:data.assetPrefix}})(),runtime:(()=>{const state=window as unknown as {next?:{router?:{isReady?:boolean}};__MIDDLEWARE_MATCHERS?:unknown;__SSG_MANIFEST?:unknown};return {routerReady:state.next?.router?.isReady,middleware:!!state.__MIDDLEWARE_MATCHERS,ssgManifest:!!state.__SSG_MANIFEST}})(),resources:performance.getEntriesByType('resource').map(entry=>({path:new URL(entry.name).pathname,start:entry.startTime,duration:entry.duration}))})).catch(()=>null); + console.error('Canvas agent UI diagnostics',JSON.stringify({url:page.url(),browserConnected:browser.isConnected(),headings,statuses,documentState,navigation,resources,pending:[...pending].map(requestName),network:network.slice(-20),canvasRequests:canvasRequests.slice(-20),errors})); throw error; }finally{await context.close();await browser.close()} } diff --git a/compose.creative-diagnostics.yml b/compose.creative-diagnostics.yml new file mode 100644 index 0000000..803cd1e --- /dev/null +++ b/compose.creative-diagnostics.yml @@ -0,0 +1,5 @@ +# Read-only test instrumentation against the already built review images. +services: + tests: + volumes: + - ./apps/waterfall-wiki/tests/canvas-ui-integration.ts:/app/tests/canvas-ui-integration.ts:ro diff --git a/scripts/integration.sh b/scripts/integration.sh index d08dad7..167fe7e 100644 --- a/scripts/integration.sh +++ b/scripts/integration.sh @@ -12,6 +12,7 @@ if [[ -n "${STUDIO_REVIEW_IMAGE:-}" ]]; then COMPOSE_FILES+=(-f compose.creative-review.yml) STUDIO_SERVICES+=(prompt-studio) fi +if [[ "${CREATIVE_BROWSER_DIAGNOSTICS:-}" == "1" ]]; then COMPOSE_FILES+=(-f compose.creative-diagnostics.yml); fi compose(){ docker compose -p "$COMPOSE_PROJECT_NAME" --env-file "$TEST_ENV" "${COMPOSE_FILES[@]}" "$@"; } cleanup(){ code=$?; if [ "$code" -ne 0 ]; then compose logs --tail 160 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}"; fi; compose down -v --remove-orphans; rm -f "$TEST_ENV"; } trap cleanup EXIT