// Isolated finite-budget regression: local fixtures only, no platform or model. import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createServer } from 'node:http'; import { fileURLToPath } from 'node:url'; const ROOT=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../..'); await fs.mkdir(path.join(ROOT,'.tmp'),{recursive:true}); const runRoot=await fs.mkdtemp(path.join(ROOT,'.tmp','batch-deadline-')); process.env.ASSISTANT_LEARNING_HOME=path.join(runRoot,'learning'); process.env.ASSISTANT_BROWSER_CHANNEL ||= process.platform==='win32' ? 'msedge' : 'chromium'; process.env.ASSISTANT_BROWSER_HEADLESS='1'; const {dispatch,verify,withBatchDeadline,withVerifyDeadline}=await import('./browser-worker.mjs'); const results=[],tasks=new Set(); let serial=0,writeRequests=0,fontRequests=0; const sockets=new Set(); const server=createServer((request,response)=>{ const url=new URL(request.url,'http://127.0.0.1'); if(url.pathname==='/pending-font'){fontRequests+=1;return;} if(url.pathname==='/write'){writeRequests+=1;response.end('unexpected');return;} if(url.pathname!=='/fixture'){response.writeHead(404).end();return;} const mode=url.searchParams.get('mode') || 'normal'; response.setHeader('Content-Type','text/html; charset=utf-8'); response.setHeader('Set-Cookie','batch-fixture-session=retained; Path=/; Max-Age=3600; SameSite=Lax'); response.end(`Local batch deadline fixture

Already read local detail

${request.headers.cookie?.includes('batch-fixture-session=retained') ? 'retained' : 'fresh'}

${['a','b','c','d','e'].map(id=>`
Author ${id}
Comment ${id}
`).join('')}
`); }); server.on('connection',socket=>{sockets.add(socket);socket.once('close',()=>sockets.delete(socket));}); await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); const base=`http://127.0.0.1:${server.address().port}/fixture`; const read={kind:'read',selector:'#detail',output_name:'detail'}; const comments={kind:'extract',selector:'#comments-root',row_selector:'.comment-row',output_name:'comments',limit:10,complete_only:true, fields:[{name:'item_id',source:'attribute',attribute:'data-item-id',required:true},{name:'author',selector:'.comment-author',source:'text',required:true},{name:'body',selector:'.comment-body',source:'text',required:true}], batch:{scroll_selector:'#comments-window',pixels:80,max_batches:3,dedupe_field:'item_id'}}; const open=async mode=>{ const task=`batch-deadline-${++serial}`,url=base+'?mode='+mode; tasks.add(task); await dispatch('open',{task_id:task,account_key:'local-batch-deadline',entry_url:url}); await dispatch('ready',{task_id:task,identity_label:'Local fixture'}); return {task,url}; }; const close=async task=>{await dispatch('close',{task_id:task});tasks.delete(task);}; const run=async(mode,options={})=>{ const {task,url}=await open(mode),started=Date.now(); const result=await verify({task_id:task,entry_url:url,parameters:{},steps:[read,comments],expected_outputs:{detail:'Already read local detail',comments:{state:'nonempty'}}},{batchBudgetMs:4000,...options}); await fs.writeFile(path.join(runRoot,mode+'.json'),JSON.stringify(result,null,2)); return {task,result,elapsed_ms:Date.now()-started}; }; const assertEvidence=async result=>{ assert.equal(new Set(result.evidence.map(item=>item.capture_id)).size,result.evidence.length); for(const capture of result.evidence){ const png=await fs.readFile(capture.screenshot_path); assert.equal(png.subarray(1,4).toString(),'PNG'); } assert.ok(result.step_timings.every(step=>Object.values(step.phase_ms).reduce((sum,value)=>sum+value,0)===step.duration_ms)); const saved=JSON.stringify(result);await new Promise(resolve=>setTimeout(resolve,180)); assert.equal(JSON.stringify(result),saved,'late browser/close callbacks must not mutate returned outputs or evidence'); }; const checkClosed=async({task,result,elapsed_ms},code)=>{ assert.equal(result.passed,false);assert.deepEqual([...result.error_codes].sort(),(Array.isArray(code) ? [...code] : [code]).sort(),JSON.stringify(result.errors)); assert.equal(result.session_closed,true);assert.equal((await dispatch('status',{task_id:task})).status,'closed'); assert.ok(elapsed_ms<8500,'internal deadline must cancel the ordinary 10s screenshot wait'); assert.ok(result.outputs.detail?.[0]?.text.includes('Already read local detail')); assert.ok(result.step_timings.at(-1).phase_ms.batch_capture>0,'the slow operation must occur in actual batch capture'); await assertEvidence(result);await close(task); }; try{ let cleaned=0; assert.equal(await withBatchDeadline(async()=>17,{budgetMs:100,cleanupReserveMs:10,onTimeout:()=>{cleaned+=1;}}),17); let release; await assert.rejects(()=>withBatchDeadline(()=>new Promise((_,reject)=>{release=reject;}),{budgetMs:60,cleanupReserveMs:15,onTimeout:()=>{cleaned+=1;release(new Error('cancelled'));}}),error=>error.code==='BATCH_BUDGET'); assert.equal(cleaned,1); let finished=false; await assert.rejects(()=>withBatchDeadline(async()=>{const end=Date.now()+45;while(Date.now(){cleaned+=1;}}),error=>error.code==='BATCH_BUDGET'); assert.equal(finished,true);assert.equal(cleaned,2,'late resolution is rejected and cleanup occurs once'); results.push({name:'batch helper normal/cancel/late-resolution semantics'}); await assert.rejects(()=>withVerifyDeadline(()=>withBatchDeadline(()=>new Promise(()=>{}),{budgetMs:500,cleanupReserveMs:30}),{budgetMs:50,cleanupReserveMs:10,onTimeout:()=>{}}),error=>error.code==='VERIFY_BUDGET'); results.push({name:'outer VERIFY_BUDGET wins before a longer batch deadline'}); const normal=await run('normal'); assert.equal(normal.result.passed,true,JSON.stringify(normal.result.errors));assert.deepEqual(normal.result.outputs.comments.items.map(item=>item.item_id),['a','b','c','d']); assert.equal(normal.result.outputs.comments.batch_summary.batches_read,3);await assertEvidence(normal.result);await close(normal.task); results.push({name:'normal finite batches retain all real PNGs and unique rows',evidence:normal.result.evidence.length}); const first=await run('first-font'); await checkClosed(first,'BATCH_BUDGET');assert.equal(first.result.outputs.comments,undefined,'uncommitted first rows cannot become an empty/success output'); assert.ok(!first.result.evidence.some(item=>item.note==='有界读取第 1 批'),'unfinished screenshot must not fabricate a first batch'); results.push({name:'first real batch screenshot cutoff retains detail but no comment output',elapsed_ms:first.elapsed_ms,evidence:first.result.evidence.length}); const later=await run('later-font'); await checkClosed(later,'BATCH_BUDGET'); assert.deepEqual(later.result.outputs.comments.items.map(item=>item.item_id),['a','b']); assert.equal(later.result.outputs.comments.truncated,true);assert.deepEqual(later.result.outputs.comments.batch_summary,{batches_read:1,duplicates_removed:0,stop_reason:'time_budget'}); assert.ok(later.result.evidence.some(item=>item.note==='有界读取第 1 批')); results.push({name:'later screenshot cutoff exposes only committed prefix with actual first PNG',elapsed_ms:later.elapsed_ms,comments:later.result.outputs.comments.count,evidence:later.result.evidence.length}); const laterBlocked=await run('later-font-blocked'); await checkClosed(laterBlocked,['BATCH_BUDGET','WRITE_REQUEST_BLOCKED']); assert.equal(laterBlocked.result.outputs.comments,undefined,'a blocked request during the unfinished screenshot must not be hidden by the previous committed prefix'); assert.ok(laterBlocked.result.evidence.some(item=>item.note==='有界读取第 1 批'),'the known-good first screenshot remains failure evidence'); assert.ok(laterBlocked.result.step_timings.at(-1).phase_ms.batch_capture>=500,'the delayed request must occur during actual screenshot waiting'); assert.ok(!JSON.stringify(laterBlocked.result).includes('LOCAL-LATE-QUERY')); assert.ok(!JSON.stringify(laterBlocked.result).includes('LOCAL-LATE-BODY')); results.push({name:'unknown POST during a later pending screenshot preserves both errors and suppresses comments',elapsed_ms:laterBlocked.elapsed_ms,evidence:laterBlocked.result.evidence.length,error_codes:laterBlocked.result.error_codes}); const global=await run('global-font',{batchBudgetMs:8000,budgetMs:4500,cleanupReserveMs:500}); await checkClosed(global,'VERIFY_BUDGET');assert.equal(global.result.outputs.comments,undefined); results.push({name:'real screenshot interrupted by outer deadline keeps VERIFY_BUDGET priority',elapsed_ms:global.elapsed_ms}); for(const [mode,code] of [['source','SOURCE_CHANGED'],['root','SOURCE_CHANGED'],['blocked','WRITE_REQUEST_BLOCKED']]){ const rejection=await run(mode); assert.equal(rejection.result.passed,false);assert.deepEqual(rejection.result.error_codes,[code],JSON.stringify(rejection.result.errors)); assert.equal(rejection.result.outputs.comments,undefined,'non-timeout safety failures must not expose the staged/earlier batch as successful comments'); assert.ok(rejection.result.evidence.some(item=>item.note==='有界读取第 1 批')); await assertEvidence(rejection.result);await close(rejection.task); results.push({name:mode+' guard is not hidden by a partial output',evidence:rejection.result.evidence.length}); } assert.equal(writeRequests,0,'unknown POST must never reach local fixture server');assert.ok(fontRequests>=4); const reopen=await open('normal'),capture=await dispatch('capture',{task_id:reopen.task}); assert.ok(capture.text.includes('retained'),'closed contexts preserve local profile login/cookie data');await close(reopen.task); results.push({name:'profile survives cancellation and a new explicit ready'}); for(const invalid of [0,15001,Infinity,1.5]) await assert.rejects(()=>verify({}, {batchBudgetMs:invalid}),error=>error.code==='INVALID_INPUT'); results.push({name:'internal test budget cannot expand the 15000 ms production cap'}); await fs.writeFile(path.join(runRoot,'report.json'),JSON.stringify({passed:results.length,results,platform_requests:0,model_calls:0,write_requests:writeRequests,font_requests:fontRequests},null,2)); console.log(JSON.stringify({passed:results.length,artifacts:runRoot,browser_channel:process.env.ASSISTANT_BROWSER_CHANNEL,platform_requests:0,model_calls:0,write_requests:writeRequests})); }catch(error){ await fs.writeFile(path.join(runRoot,'report.json'),JSON.stringify({passed:results.length,results,error:{message:error.message,code:error.code,stack:error.stack},platform_requests:0,model_calls:0,write_requests:writeRequests},null,2)); console.error(JSON.stringify({passed:results.length,artifacts:runRoot,error:error.message,code:error.code}));process.exitCode=1; }finally{ await Promise.allSettled([...tasks].map(task=>dispatch('close',{task_id:task}))); for(const socket of sockets)socket.destroy(); await new Promise(resolve=>server.close(resolve)); }