Compare commits

...

18 Commits

Author SHA1 Message Date
codex-bot 57fdf3a48d Exclude task whose active PR changed during cleanup audit
runner verified cross-task image recovery / diagnose (push) Successful in 2m31s
2026-10-31 04:21:18 +08:00
codex-bot 26333847fb Recover verified old task images while retaining live and recent versions 2026-10-31 04:19:34 +08:00
codex-bot 2def86baf7 Inventory tagged images across tasks before scoped cleanup
runner tagged image ownership inventory / diagnose (push) Successful in 22s
2026-10-31 04:11:09 +08:00
codex-bot e2c61801dc Recover only old dangling image cache without tagged releases or volumes
runner old dangling image cache recovery / diagnose (push) Successful in 4m32s
2026-10-31 04:03:07 +08:00
codex-bot c9f28025f0 Recover ended failed builds while preserving live and rollback releases
creative verified failed-image disk recovery / diagnose (push) Successful in 1m47s
2026-10-31 03:57:18 +08:00
codex-bot 3deed0861c Plan owned review tags and old build cache using image ancestry
creative image ownership and physical disk plan / diagnose (push) Successful in 9m51s
2026-10-31 03:46:21 +08:00
codex-bot dce2998396 Recover verified terminal review tags and inventory disk owners
creative disk recovery and inventory / diagnose (push) Successful in 5m10s
2026-10-31 03:38:47 +08:00
codex-bot b33d3a6b8f Handle unlabelled Studio images in read-only tag inventory
creative review image cleanup preview / diagnose (push) Successful in 11s
2026-10-31 03:18:29 +08:00
codex-bot 9de10d0a62 Preview only completed canvas review image tags for disk recovery
creative review image cleanup preview / diagnose (push) Failing after 1m35s
2026-10-31 03:16:12 +08:00
codex-bot fd8cff3c26 Inspect runner pressure and cleanup after release 611 timeout
creative runner pressure diagnosis / diagnose (push) Successful in 8s
2026-10-31 03:14:10 +08:00
codex-bot 2b9e40b1aa Clean only confirmed cancelled run 605 disposable stack
creative cancelled stack cleanup / diagnose (push) Successful in 15s
2026-10-31 02:12:06 +08:00
codex-bot 566362a3f3 Inspect disposable resources left by cancelled review run 605
creative cancelled stack diagnosis / diagnose (push) Successful in 1s
2026-10-31 01:57:12 +08:00
codex-bot f596ef8c40 Inspect Docker driver and image sizes without starting containers
creative runner storage diagnosis / diagnose (push) Successful in 0s
2026-10-30 23:44:17 +08:00
codex-bot 159da3e1f1 Trace guidance session timing and runner resource pressure
creative browser diagnosis / diagnose (push) Successful in 4m33s
2026-10-30 23:24:10 +08:00
codex-bot efdfb6a2ff Verify mobile account readiness with its intentionally hidden link
creative browser diagnosis / diagnose (push) Failing after 3m48s
2026-10-30 23:19:14 +08:00
codex-bot 52df5b8e32 Compare the same app images with Playwright matched Chromium
creative browser diagnosis / diagnose (push) Failing after 6m7s
2026-10-30 23:11:49 +08:00
codex-bot 82e83fc8cd Isolate canvas probe and report failing HTTP paths and container state
creative browser diagnosis / diagnose (push) Failing after 4m34s
2026-10-30 23:06:40 +08:00
codex-bot c97d36bbb2 Add isolated browser diagnostics using existing review images
creative browser diagnosis / diagnose (push) Failing after 2m1s
2026-10-30 23:03:27 +08:00
6 changed files with 122 additions and 9 deletions
File diff suppressed because one or more lines are too long
@@ -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(/<script[^>]+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<Request>(),network:string[]=[];
const resources:{ms:number;event:string;path:string;status?:number;type?:string;duration?:number}[]=[],requestStarts=new Map<Request,number>();
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'});
@@ -98,7 +110,7 @@ export async function verifyCanvasAgentUI(base:string){
await page.setViewportSize({width:390,height:844});
const bounds=await panel.boundingBox();assert.ok(bounds&&bounds.x>=0&&bounds.x+bounds.width<=390,'Agent panel stays within the mobile viewport');
await page.reload({waitUntil:'domcontentloaded'});
await page.getByRole('link',{name:email,exact:true}).waitFor({state:'attached'});
await page.getByRole('link',{name:email,exact:true,includeHidden:true}).waitFor({state:'attached'});
await page.getByRole('button',{name:'Agent',exact:true}).click();
const reopened=page.getByRole('region',{name:'Agent 连接',exact:true});
await reopened.getByRole('combobox',{name:'打开在线画布',exact:true}).selectOption(list.canvases[0].id);
@@ -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()}
}
@@ -0,0 +1,14 @@
import {test} from 'node:test';
import {chromium} from 'playwright-core';
import {verifyGuidanceWorkspaceUI} from './guidance-workspace-ui';
const originalFetch=globalThis.fetch;
globalThis.fetch=async(input,init)=>{
const url=typeof input==='string'?input:input instanceof URL?input.href:input.url;
const path=new URL(url).pathname,started=Date.now();
try{const response=await originalFetch(input,init);console.log('Diagnostic HTTP',JSON.stringify({path,method:init?.method||'GET',status:response.status,ms:Date.now()-started}));return response}
catch(error){console.error('Diagnostic HTTP failed',JSON.stringify({path,method:init?.method||'GET',ms:Date.now()-started,error:error instanceof Error?error.message:String(error)}));throw error}
};
test('diagnose guidance session readiness against the same images',{timeout:300000},async()=>{
const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});
try{await verifyGuidanceWorkspaceUI(process.env.PROMPT_TEST_BASE_URL!,browser)}finally{await browser.close()}
});
@@ -27,9 +27,11 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
requestedAuthor: string | null = null,
published: Guidance | null = null;
const errors: string[] = [];
const started=performance.now(),trace:{ms:number;event:string;path?:string}[]=[];
const mark=(event:string,path?:string)=>trace.push({ms:Math.round(performance.now()-started),event,path});
try {
await context.route("**/api/account/session", (route) =>
route.fulfill({
(mark("session route handler"),route.fulfill({
json: {
user: signedIn
? {
@@ -39,7 +41,7 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
}
: null,
},
}),
})),
);
await context.route(
(url) => url.pathname === "/api/documents",
@@ -87,6 +89,8 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
);
const page = await context.newPage();
page.setDefaultTimeout(10000);
page.on('domcontentloaded',()=>mark('domcontentloaded',new URL(page.url()).pathname));
page.on('response',response=>{if(new URL(response.url()).pathname==='/api/account/session')mark('session response '+response.status())});
page.on("pageerror", (error) => errors.push(error.message));
page.on("crash", () => errors.push("Browser renderer crashed"));
const pending = new Set<string>(), network: string[] = [];
@@ -94,12 +98,14 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
page.on("requestfinished", request => pending.delete(request.url()));
page.on("requestfailed", request => {pending.delete(request.url());network.push(new URL(request.url()).pathname+": "+request.failure()?.errorText);});
async function navigate(url: string, reload = false) {
mark("navigate begin",new URL(url).pathname);
const session = page.waitForResponse(response => new URL(response.url()).pathname === "/api/account/session" && response.status() === 200);
try {
const [navigation] = await Promise.all([reload ? page.reload({waitUntil:"domcontentloaded"}) : page.goto(url,{waitUntil:"domcontentloaded"}),session]);
assert.ok(navigation?.ok(), "Navigation returns a successful document");
await page.getByText(signedIn ? "editor@example.com" : "登录你的账户",{exact:true}).first().waitFor({state:"attached"});
} catch (error) {
console.error("Guidance ordered readiness",JSON.stringify(trace));
const probeStarted=Date.now();
const server=await fetch(url,{signal:AbortSignal.timeout(3000)}).then(async response=>{await response.body?.cancel();return {status:response.status,elapsedMs:Date.now()-probeStarted}}).catch(error=>({error:error instanceof Error?error.name:"unknown",elapsedMs:Date.now()-probeStarted}));
console.error("Workspace UI navigation diagnostics",JSON.stringify({url:page.url(),browserConnected:browser.isConnected(),server,pending:[...pending].map(value=>new URL(value).pathname),network:network.slice(-20),errors,body:(await page.locator("body").innerText().catch(()=>"unavailable")).slice(0,1600)}));
+7
View File
@@ -0,0 +1,7 @@
# 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
- ./apps/waterfall-wiki/tests/creative-browser-diagnostics.test.ts:/app/tests/integration.test.ts:ro
- ./apps/waterfall-wiki/tests/guidance-workspace-ui.ts:/app/tests/guidance-workspace-ui.ts:ro
+15 -1
View File
@@ -12,8 +12,21 @@ 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"; }
diagnostic_state(){
if [[ "${CREATIVE_BROWSER_DIAGNOSTICS:-}" != "1" ]]; then return; fi
for metric in /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io; do
if [[ -r "$metric" ]]; then echo "$metric"; cat "$metric"; fi
done
local ids
ids=$(compose ps -aq)
if [[ -n "$ids" ]]; then
docker inspect --format '{{.Name}} OOMKilled={{.State.OOMKilled}} RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}}' $ids
docker stats --no-stream --format '{{.Name}} CPU={{.CPUPerc}} Memory={{.MemUsage}} PIDs={{.PIDs}}' $ids
fi
}
cleanup(){ code=$?; diagnostic_state || true; 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
# Random credentials exist only for this disposable stack; no host Node required.
docker run --rm --entrypoint node "$WATERFALL_WIKI_IMAGE" -e "const c=require('crypto');const hex=()=>c.randomBytes(24).toString('hex');console.log('POSTGRES_PASSWORD='+hex()+'\nS3_ACCESS_KEY='+hex()+'\nS3_SECRET_KEY='+hex())" > "$TEST_ENV"
@@ -23,6 +36,7 @@ if [[ -n "${STUDIO_REVIEW_IMAGE:-}" ]]; then printf '\nACCOUNT_PUBLIC_ORIGIN=htt
compose up -d --no-build --wait --wait-timeout 120 postgres objects
compose run --rm --no-deps migrate
compose up -d --no-build --wait --wait-timeout 150 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}"
diagnostic_state
# The test runner calls the actual non-root production web/worker containers.
compose run --rm --no-deps tests
compose exec -T waterfall-wiki node container-health.mjs