lazyBoy/tests/frontend.test.mjs

886 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {test} from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
import ts from '../apps/web/node_modules/typescript/lib/typescript.js';
function loadCatalog(file){
const source=fs.readFileSync(file,'utf8');
// transpileModule prints output even for a catalog that does not parse, so a
// missing comma silently loads a shorter catalog and every key check below
// still passes. Ask the parser, which actually keeps its errors.
const parsed=ts.createSourceFile(file,source,ts.ScriptTarget.ESNext,true,ts.ScriptKind.TS);
assert.deepEqual((parsed.parseDiagnostics||[]).map(d=>ts.flattenDiagnosticMessageText(d.messageText,' ')),[],`${file} must parse`);
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const sandbox={exports:{},require:()=>({})};
vm.runInNewContext(code,sandbox);
return sandbox.exports.zhTW||sandbox.exports.en;
}
const zhTW=loadCatalog('apps/web/src/locales/zh-TW.ts');
const en=loadCatalog('apps/web/src/locales/en.ts');
const catalogs={'zh-TW':zhTW,en};
const i18n={locale:'en',t:(key,params)=>{const message=catalogs[i18n.locale]?.[key]??key;return params?message.replace(/\{(\w+)\}/g,(m,name)=>Object.hasOwn(params,name)?String(params[name]):m):message},getLocale:()=>i18n.locale};
const raw=fs.readFileSync('apps/web/src/schedule.tsx','utf8');
const js=ts.transpileModule(raw,{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.React}}).outputText;
const box={exports:{},require:()=>i18n};vm.runInNewContext(js,box);const {cronFromPreset,presetFromCron,defaultCronPreset,describeCron,describeCronHuman}=box.exports;
test('fixed intervals retain exact elapsed units',()=>{
for (const unit of ['minutes','hours','days']) {const p={...defaultCronPreset(),freq:'Interval',n:45,unit};const cron=cronFromPreset(p);const restored=presetFromCron(cron);assert.equal(restored.n,45);assert.equal(restored.unit,unit);}
});
test('unknown cron is preserved verbatim instead of silently rewritten',()=>{
for(const cron of ['0 0 9 * * 1','0 25 * * *','99 9 * * *','0 0 */3 * *','*/45 * * * *']){const p=presetFromCron(cron);assert.equal(p.freq,'Advanced');assert.equal(cronFromPreset(p),cron);}
});
test('blank advanced expressions are never converted to a scheduled job',()=>assert.equal(cronFromPreset({...defaultCronPreset(),freq:'Advanced',cron:''}),''));
test('VNC paste delegates once to confirmed backend and rejects another source',async()=>{
const source=fs.readFileSync('apps/web/vnc.html','utf8').match(/<script type="module">([\s\S]*?)<\/script>/)[1].replace(/import RFB[^;]+;/,'');
const handlers={},sent=[];class RFB{addEventListener(){} focus(){} sendKey(){}}
const window={location:{pathname:'/vnc.html',protocol:'http:',host:'localhost',origin:'http://localhost',hash:''},parent:{postMessage:x=>sent.push(x)},addEventListener:(n,f)=>handlers[n]=f};
vm.runInNewContext(source,{window,document:{location:{href:'http://localhost/vnc.html?view_only=false'},getElementById:()=>({}),querySelector:()=>null},navigator:{clipboard:{readText:async()=>'中文\nhello'}},RFB,setTimeout(){},clearTimeout(){}});
await handlers.keydown({ctrlKey:true,code:'KeyV',preventDefault(){},stopImmediatePropagation(){}});
assert.equal(sent.filter(x=>x.type==='lazyboy-paste-text').length,1);
assert.equal(sent.at(-1).text,'中文\nhello');
handlers.message({origin:'http://localhost',source:{},data:{type:'lazyboy-host-clipboard',text:'bad'}});assert.equal(sent.at(-1).text,'中文\nhello');
});
const mdJs=ts.transpileModule(fs.readFileSync('apps/web/src/markdown.tsx','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const mdBox={exports:{},require:(name)=>{
if(name==='react')return{useCallback:fn=>fn,useRef:()=>({current:null}),useState:()=>[false,()=>{}],useMemo:fn=>fn(),memo:fn=>fn,Children:{map:(n,fn)=>n},cloneElement:el=>el,isValidElement:()=>false};
if(name==='react/jsx-runtime')return{jsx:()=>null,jsxs:()=>null,Fragment:'Fragment'};
if(name==='react-markdown'||name==='remark-gfm'||name==='remark-breaks')return{default:()=>null};
if(name==='./i18n')return{t:key=>key};
if(name==='./mentions')return{mentionSpans:()=>[]};
if(name.endsWith('.css'))return{};
throw new Error('unexpected import '+name);
}};
vm.runInNewContext(mdJs,mdBox);
const {sanitizeMarkdownUrl}=mdBox.exports;
const audioJs=ts.transpileModule(fs.readFileSync('apps/web/src/call-audio.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const audioBox={exports:{},require:()=>{throw new Error('unexpected import')}};
vm.runInNewContext(audioJs,audioBox);
const {floatToPcm16,pcm16ToFloat,resample,VOICE_SAMPLE_RATE,CAPTURE_CHUNK_SAMPLES}=audioBox.exports;
function fakePlaybackContext(rendered){
return {
sampleRate:VOICE_SAMPLE_RATE,currentTime:0,
createBuffer:(_channels,length,rate)=>{const data=new Float32Array(length);rendered.push(data);return {length,duration:length/rate,getChannelData:()=>data}},
createBufferSource:()=>({buffer:null,connect(){},start(){},stop(){}}),
};
}
test('pcm16 roundtrip keeps amplitude sign and resample identity at 24 kHz',()=>{
const source=new Float32Array([0,0.5,-0.5,1,-1]);
const bytes=floatToPcm16(source);
const back=pcm16ToFloat(bytes.buffer);
assert.equal(back.length,source.length);
assert.ok(back[1]>0.49&&back[1]<0.51);
assert.ok(back[2]< -0.49&&back[2]> -0.51);
const same=resample(source,VOICE_SAMPLE_RATE,VOICE_SAMPLE_RATE);
assert.equal(same,source);
const down=resample(new Float32Array([0,1,0,1]),48000,24000);
assert.equal(down.length,2);
});
test('markdown links only keep http(s), mailto, tel, and in-page hashes',()=>{
assert.equal(sanitizeMarkdownUrl('https://example.com/docs'),'https://example.com/docs');
assert.equal(sanitizeMarkdownUrl('mailto:hi@example.com'),'mailto:hi@example.com');
assert.equal(sanitizeMarkdownUrl('#section'),'#section');
assert.equal(sanitizeMarkdownUrl('javascript:alert(1)'),undefined);
assert.equal(sanitizeMarkdownUrl('data:text/html,<script>alert(1)</script>'),undefined);
assert.equal(sanitizeMarkdownUrl('/relative'),undefined);
});
test('zh-TW and en catalogs share keys and placeholders',()=>{
const zhKeys=Object.keys(zhTW).sort();
const enKeys=Object.keys(en).sort();
assert.deepEqual(enKeys,zhKeys);
for(const key of zhKeys){
const slots=s=>[...String(s).matchAll(/\{(\w+)\}/g)].map(m=>m[1]).sort();
assert.deepEqual(slots(en[key]),slots(zhTW[key]),key);
assert.notEqual(String(en[key]).trim(),'');
assert.notEqual(String(zhTW[key]).trim(),'');
}
});
test('describeCron and stored Chinese human strings follow the UI locale',()=>{
i18n.locale='en';
assert.equal(describeCron('0 9 * * *'),'Daily at 9:00 AM');
assert.equal(describeCron('0 9 * * 1-5'),'Weekdays at 9:00 AM');
assert.equal(describeCronHuman('每天 09:00'),'Daily at 9:00 AM');
assert.equal(describeCronHuman('每隔 45 分鐘(固定間隔)'),'Every 45 minutes (elapsed)');
i18n.locale='zh-TW';
assert.equal(describeCron('0 9 * * *'),'每天 09:00');
assert.equal(describeCronHuman('Daily at 9:00 AM'),'每天 09:00');
});
test('hanging up before microphone permission resolves releases the late stream',async()=>{
let grant; let stopped=0;
const context={exports:{},navigator:{mediaDevices:{getUserMedia:()=>new Promise(resolve=>grant=resolve)}}};
vm.runInNewContext(audioJs,context);
const audio=new context.exports.CallAudio({onCapture(){},onError(){}});
const starting=audio.start();
audio.stop();
grant({getTracks:()=>[{stop(){stopped++;}}]});
await starting;
assert.equal(stopped,1);
});
test('odd-sized audio frames stay sample-aligned instead of turning into noise',()=>{
const rendered=[];
const audio=new audioBox.exports.CallAudio({onCapture(){},onError(){}});
audio.context=fakePlaybackContext(rendered);
const source=new Int16Array([1000,-1000,2000,-2000,3000,-3000,4000,-4000]);
const bytes=new Uint8Array(source.buffer);
let offset=0;
for(const size of [3,5,7,1]){audio.play(bytes.slice(offset,offset+size).buffer);offset+=size}
const played=rendered.flatMap(chunk=>[...chunk]);
const expected=[...pcm16ToFloat(bytes.slice().buffer)];
assert.equal(played.length,expected.length);
expected.forEach((value,index)=>assert.ok(Math.abs(played[index]-value)<1e-6,`sample ${index}`));
});
test('microphone frames are batched instead of sent one websocket message per block',()=>{
const sent=[];
const audio=new audioBox.exports.CallAudio({onCapture:pcm=>sent.push(pcm.length),onError(){}});
const block=new Float32Array(64);
const blocksPerChunk=CAPTURE_CHUNK_SAMPLES/block.length;
for(let i=0;i<blocksPerChunk-1;i++)audio.queueCapture(block);
assert.deepEqual(sent,[]);
audio.queueCapture(block);
assert.deepEqual(sent,[CAPTURE_CHUNK_SAMPLES*2]);
});
function mobileViewer(){
const source=fs.readFileSync('apps/web/vnc.html','utf8').match(/<script type="module">([\s\S]*?)<\/script>/)[1].replace(/import RFB[^;]+;/,'');
const handlers={},options={},sent=[];
const keyboard={value:'',focused:false,focus(){this.focused=true},blur(){this.focused=false},setSelectionRange(){}};
const pointerEvents=[];
const canvas={style:{},closest:selector=>selector==='#screen'?{}:null,getBoundingClientRect:()=>({left:0,top:0,width:300,height:200})};
const elements={};
const element=id=>elements[id]||(elements[id]={style:{},attributes:{},setAttribute(k,v){this.attributes[k]=v},querySelectorAll(){return []}});
let rfb;
class RFB{constructor(){rfb=this}addEventListener(){}focus(){keyboard.focused=false}
_handleMouseMove(x,y){pointerEvents.push({type:'move',x,y})}
_handleMouseButton(x,y,mask){pointerEvents.push({type:'button',x,y,mask})}
_handleWheel(event){pointerEvents.push({type:'wheel',dx:event.deltaX,dy:event.deltaY})}
}
const body={className:'',classList:{tokens:new Set(),toggle(name,force){if(force===undefined)force=!this.tokens.has(name);if(force)this.tokens.add(name);else this.tokens.delete(name);body.className=[...this.tokens].join(' ')}}};
const window={location:{pathname:'/vnc.html',protocol:'http:',host:'localhost',origin:'http://localhost',hash:''},parent:{postMessage:x=>sent.push(x)},addEventListener:(n,f,o)=>{handlers[n]=f;options[n]=o},matchMedia:()=>({matches:false,addEventListener(){}})};
vm.runInNewContext(source,{window,document:{body,location:{href:'http://localhost/vnc.html?view_only=false'},getElementById:id=>id==='mobile-keyboard'?keyboard:element(id),querySelector:selector=>selector==='#screen canvas'?canvas:null},navigator:{},RFB,setTimeout(){},clearTimeout(){}});
return {handlers,options,sent,keyboard,canvas,rfb,window,pointerEvents,elements,body};
return {handlers,options,sent,keyboard,canvas,rfb,window,pointerEvents,elements};
}
test('direct mobile taps position without unexpectedly opening the keyboard',()=>{
const {handlers:h,options,keyboard,canvas,rfb}=mobileViewer();
for(const name of ['touchstart','touchmove','touchend'])assert.equal(options[name].capture,true);
const point={clientX:20,clientY:30};
h.touchstart({target:canvas,touches:[point]});h.touchend({touches:[]});
assert.equal(keyboard.focused,false);assert.equal(rfb.focusOnClick,false);
keyboard.blur();h.touchstart({target:canvas,touches:[point]});h.touchmove({touches:[{clientX:50,clientY:30}]});h.touchend({touches:[]});assert.equal(keyboard.focused,false);
h.touchstart({target:canvas,touches:[point,point]});h.touchend({touches:[]});assert.equal(keyboard.focused,false);
rfb.viewOnly=true;h.touchstart({target:canvas,touches:[point]});h.touchend({touches:[]});assert.equal(keyboard.focused,false);
});
test('mobile IME sends committed Chinese once and supports input-only backspace and Enter',()=>{
const {handlers:h,keyboard,sent}=mobileViewer();
sent.length=0;
h.compositionstart({target:keyboard});keyboard.value+='中文';
h.input({target:keyboard,isComposing:true});assert.equal(sent.length,0);
h.compositionend({target:keyboard});h.input({target:keyboard,isComposing:false});
assert.equal(sent.length,1);assert.equal(sent[0].text,'中文');
keyboard.value='';h.input({target:keyboard,inputType:'deleteContentBackward'});
let prevented=false;h.beforeinput({target:keyboard,inputType:'insertLineBreak',preventDefault(){prevented=true}});
assert.equal(prevented,true);
assert.deepEqual(sent.map(x=>x.text||x.key),['中文','BackSpace','Return']);
});
test('mobile keyboard is cleared and blurred when control is released; foreign messages cannot change control',()=>{
const {handlers:h,keyboard,sent,window}=mobileViewer();
keyboard.focus();keyboard.value+='secret';
h.message({origin:window.location.origin,source:{},data:{type:'lazyboy-view-only',viewOnly:true}});
assert.equal(keyboard.focused,true);
h.message({origin:window.location.origin,source:window.parent,data:{type:'lazyboy-view-only',viewOnly:true}});
assert.equal(keyboard.focused,false);assert.equal(keyboard.value,'\u200b');
sent.length=0;keyboard.value+='blocked';h.input({target:keyboard});assert.equal(sent.length,0);
});
function pointerButton(viewer,action){
viewer.handlers.click({target:{closest:()=>({dataset:{action}})}});
}
function pointerTouch(viewer,name,points){
let prevented=false,stopped=false;
viewer.handlers[name]({target:{closest:selector=>selector==='#trackpad'?{}:null},touches:points.map(([clientX,clientY])=>({clientX,clientY})),preventDefault(){prevented=true},stopImmediatePropagation(){stopped=true}});
return prevented&&stopped;
}
test('trackpad moves relative to cursor, clamps edges and taps without opening keyboard',()=>{
const v=mobileViewer();pointerButton(v,'mode');
assert.equal(pointerTouch(v,'touchstart',[[30,30]]),true);
pointerTouch(v,'touchmove',[[60,40]]);pointerTouch(v,'touchend',[]);
assert.equal(v.pointerEvents.length,1);assert.equal(v.pointerEvents[0].x,180);assert.ok(Math.abs(v.pointerEvents[0].y-110)<.001);
assert.equal(v.keyboard.focused,false);
pointerTouch(v,'touchstart',[[10,10]]);pointerTouch(v,'touchend',[]);
assert.deepEqual(v.pointerEvents.slice(-2).map(e=>e.mask),[1,0]);
pointerTouch(v,'touchstart',[[10,10]]);pointerTouch(v,'touchmove',[[1000,-1000]]);pointerTouch(v,'touchend',[]);
assert.equal(v.pointerEvents.at(-1).x,299);assert.equal(v.pointerEvents.at(-1).y,0);
pointerButton(v,'keyboard');assert.equal(v.keyboard.focused,true);
});
test('two-finger trackpad scroll and right tap do not also left click',()=>{
const v=mobileViewer();pointerButton(v,'mode');
pointerTouch(v,'touchstart',[[20,20],[50,20]]);pointerTouch(v,'touchmove',[[20,80],[50,80]]);pointerTouch(v,'touchend',[]);
assert.deepEqual(v.pointerEvents,[{type:'wheel',dx:-0,dy:-60}]);
pointerTouch(v,'touchstart',[[20,20],[50,20]]);pointerTouch(v,'touchend',[[20,20]]);pointerTouch(v,'touchend',[]);
assert.deepEqual(v.pointerEvents.slice(-2).map(e=>e.mask),[4,0]);
});
test('drag is released on cancellation, mode switch, blur and control handoff',()=>{
for(const release of ['cancel','mode','blur','handoff']){
const v=mobileViewer();pointerButton(v,'drag');
assert.equal(v.pointerEvents.at(-1).mask,1);
if(release==='cancel')v.handlers.touchcancel();
if(release==='mode')pointerButton(v,'mode');
if(release==='blur')v.handlers.blur();
if(release==='handoff')v.handlers.message({origin:v.window.location.origin,source:v.window.parent,data:{type:'lazyboy-view-only',viewOnly:true}});
assert.equal(v.pointerEvents.at(-1).mask,0,release);
assert.equal(v.elements['pointer-drag'].attributes['aria-pressed'],'false');
}
});
test('view-only pointer controls never send mouse input and trackpad asks for control',()=>{
const v=mobileViewer();pointerButton(v,'mode');v.rfb.viewOnly=true;
for(const action of ['left','right','drag','up','down'])pointerButton(v,action);
pointerTouch(v,'touchstart',[[20,20]]);pointerTouch(v,'touchmove',[[50,50]]);pointerTouch(v,'touchend',[]);
assert.equal(v.pointerEvents.length,0);
assert.equal(v.sent.at(-1).type,'lazyboy-request-control');
});
test('trackpad mode swallows screen taps so fingers on the picture do not click',()=>{
const v=mobileViewer();pointerButton(v,'mode');
let prevented=false,stopped=false;
v.handlers.touchstart({target:v.canvas,touches:[{clientX:20,clientY:30}],preventDefault(){prevented=true},stopImmediatePropagation(){stopped=true}});
assert.equal(prevented&&stopped,true);
assert.equal(v.pointerEvents.length,0);
assert.equal(v.keyboard.focused,false);
});
test('coarse pointers start in trackpad mode with a body class for independent controls',()=>{
const source=fs.readFileSync('apps/web/vnc.html','utf8').match(/<script type="module">([\s\S]*?)<\/script>/)[1].replace(/import RFB[^;]+;/,'');
const body={className:'',classList:{tokens:new Set(),toggle(name,force){if(force)this.tokens.add(name);else this.tokens.delete(name);body.className=[...this.tokens].join(' ')}}};
const window={location:{pathname:'/vnc.html',protocol:'http:',host:'localhost',origin:'http://localhost',hash:''},parent:{postMessage(){}},addEventListener(){},matchMedia:()=>({matches:true,addEventListener(){}})};
const elements={}; const element=id=>elements[id]||(elements[id]={style:{},attributes:{},hidden:true,setAttribute(k,v){this.attributes[k]=v},querySelectorAll(){return []}});
class RFB{constructor(){} addEventListener(){} focus(){}}
vm.runInNewContext(source,{window,document:{body,location:{href:'http://localhost/vnc.html?view_only=false'},getElementById:id=>element(id),querySelector:()=>null},navigator:{},RFB,setTimeout(){},clearTimeout(){}});
assert.equal(body.className,'touch-ui');
assert.equal(elements.trackpad.hidden,false);
});
test('mobile shortcut row sends modifiers and hides with the keyboard',()=>{
const v=mobileViewer();
pointerButton(v,'keyboard');
assert.equal(v.keyboard.focused,true);
assert.equal(v.elements['keyboard-shortcuts'].hidden,false);
v.handlers.click({target:{closest:()=>({dataset:{action:'modifier',key:'ctrl'}})}});
v.handlers.click({target:{closest:()=>({dataset:{action:'key',key:'c'}})}});
assert.equal(v.sent.at(-1).type,'lazyboy-mobile-key');
assert.equal(v.sent.at(-1).key,'ctrl+c');
v.handlers.click({target:{closest:()=>({dataset:{action:'hide-keyboard'}})}});
assert.equal(v.keyboard.focused,false);
assert.equal(v.elements['keyboard-shortcuts'].hidden,true);
});
test('status sits outside the remote pixels; screenshots stay opt-in',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.doesNotMatch(app,/RunStatus/);
assert.match(app,/<RunProbe runId=\{computer\.busyRunId\|\|computer\.waitingRunId\}><Avatar/);
assert.match(app,/--visible-height/);
assert.match(app,/<div className=\{`composer-dock \$\{statusMembers\.length\?"has-status":""\}`\}>\s*\{error&&<div className="error-banner"/);
const css=fs.readFileSync('apps/web/src/computer.css','utf8');
assert.doesNotMatch(css,/\.run-status/);
const chat=fs.readFileSync('apps/web/src/chat.css','utf8');
assert.match(chat,/\.composer-dock\{position:relative/);
assert.doesNotMatch(chat,/\.composer-dock\{position:absolute/);
assert.match(chat,/\.messages\{min-width:0;padding-bottom:24px/);
const tools=fs.readFileSync('crates/api/src/tools.rs','utf8');
assert.match(tools,/"observe":\{"type":"boolean","description":"Attach a screenshot only when visual verification is needed; default false"\}/);
assert.match(tools,/if args\.get\("observe"\)\.and_then\(Value::as_bool\) != Some\(true\)/);
const runs=fs.readFileSync('crates/api/src/runs.rs','utf8');
assert.match(runs,/Never silently stop/);
assert.match(runs,/let goal_mode = goal_mode \|\| !chat_only/);
});
const monitorJs=ts.transpileModule(fs.readFileSync('apps/web/src/run-monitor.tsx','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const monitorBox={exports:{},require:name=>{
if(name==='react')return{useCallback:fn=>fn,useEffect:()=>{},useRef:()=>({current:null}),useState:()=>[null,()=>{}]};
if(name==='react/jsx-runtime')return{jsx:()=>null,jsxs:()=>null,Fragment:'Fragment'};
if(name==='./api')return{api:async()=>({activity:[]})};
if(name==='./i18n')return{t:i18n.t,getLocale:()=>i18n.locale};
throw new Error('unexpected import '+name);
}};
vm.runInNewContext(monitorJs,monitorBox);
const {formatElapsed,shortDuration,errorActions,errorTitle,trailText}=monitorBox.exports;
test('file readback UI distinguishes a matching state from proven execution',()=>{
const message=monitorBox.exports.fileEvidenceMessage;
assert.equal(message({status:'unknown',evidence:{desired_state_matches:true,execution_proven:false}}),'fileEvidenceMatches');
assert.equal(message({status:'unknown',evidence:{desired_state_matches:false}}),'fileEvidenceDiffers');
assert.equal(message({status:'unknown',evidence:{desired_state_matches:null}}),'fileEvidenceUnknown');
assert.equal(message({status:'succeeded',evidence:{verified:false}}),'fileEvidenceUnknown');
assert.equal(message({status:'succeeded',evidence:{verified:true}}),'fileEvidenceRecorded');
assert.equal(message({status:'succeeded',evidence:{verified:true,verificationBasis:'postcondition'}}),'fileEvidenceResolved');
assert.equal(message({status:'failed',evidence:null}),'fileEvidenceConflict');
});
test('historical recovery requires a central availability hint and explicit action',()=>{
const canResolve=monitorBox.exports.canResolveFileEvidence;
const available={status:'unknown',historical:true,retirementRecoveryAvailable:true};
assert.equal(canResolve(available),true);
assert.equal(monitorBox.exports.fileEvidenceMessage(available),'fileEvidenceRetiredReady');
for(const flag of [undefined,false,'true',1])assert.equal(canResolve({...available,retirementRecoveryAvailable:flag}),false);
for(const status of ['succeeded','failed'])assert.equal(canResolve({...available,status}),false);
assert.equal(canResolve(null),false);
assert.equal(canResolve({...available,historical:false}),false);
assert.equal(canResolve({status:'unknown',historical:true,evidence:{desired_state_matches:true,helper_quiescent:true,controller_quiescent:true}}),false);
assert.equal(monitorBox.exports.fileEvidenceMessage({...available,evidence:{desired_state_matches:false}}),'fileEvidenceDiffers');
});
const FAILURE_CODES=['interrupted','tool_timeout','model_key','model_key_missing','model_opt_in','model_quota','model_unknown','model_timeout','network','computer_gone','lease_lost','context_overflow','unknown'];
test('run timings stay on one glanceable line',()=>{
assert.equal(formatElapsed(0),'0:00');
assert.equal(formatElapsed(9_500),'0:09');
assert.equal(formatElapsed(65_000),'1:05');
assert.equal(formatElapsed(3_725_000),'1:02:05');
assert.equal(formatElapsed(-5),'0:00');
assert.equal(formatElapsed(NaN),'0:00');
assert.equal(shortDuration(340),'340ms');
assert.equal(shortDuration(6_400),'6.4s');
assert.equal(shortDuration(65_000),'1:05');
assert.equal(shortDuration(null),'');
assert.equal(shortDuration(-1),'');
});
test('every failure code offers at least one action, in the right order',()=>{
const buttons=code=>errorActions(code).join(',');
assert.equal(buttons('model_key'),'settings,retry');
assert.equal(buttons('model_unknown'),'settings,retry');
assert.equal(buttons('model_quota'),'retry,settings');
assert.equal(buttons('model_timeout'),'retry,settings');
assert.equal(buttons('network'),'retry,settings');
assert.equal(buttons('computer_gone'),'screen,retry');
assert.equal(buttons('tool_timeout'),'screen,retry');
assert.equal(buttons('interrupted'),'screen,retry');
assert.equal(buttons('lease_lost'),'retry');
assert.equal(buttons('context_overflow'),'retry');
for(const code of [...FAILURE_CODES,'made_up',undefined,null]){
const actions=errorActions(code);
assert.ok(actions.length>0,String(code));
assert.ok(actions.every(action=>['retry','screen','settings'].includes(action)),code);
assert.equal(new Set(actions).size,actions.length,'no duplicate buttons for '+code);
}
});
test('failure titles resolve in both catalogs and never repeat themselves',()=>{
for(const locale of Object.keys(catalogs)){
i18n.locale=locale;
const titles=FAILURE_CODES.map(code=>errorTitle(code));
for(const [index,title] of titles.entries()){
assert.notEqual(title,title.startsWith('errorTitle')?title:`${title}-missing`,FAILURE_CODES[index]);
assert.ok(!title.startsWith('errorTitle'),`${locale} ${FAILURE_CODES[index]} has no copy`);
assert.ok(title.trim().length>0);
}
assert.equal(new Set(titles).size,titles.length,locale);
assert.ok(errorTitle('made_up').length>0);
}
i18n.locale='en';
});
test('trail lines read as sentences with the detail a stuck run needs',()=>{
assert.equal(trailText({id:1,kind:'model',createdAt:'',turn:3,elapsedMs:6400,text:'先打開網頁'}),'turn 3, thought for 6.4s — 先打開網頁');
assert.match(trailText({id:2,kind:'tool',createdAt:'',step:'browser: click #12',status:'ok',elapsedMs:400}),/browser: click #12 · ok · 400ms/);
assert.match(trailText({id:3,kind:'tool',createdAt:'',step:'shell: npm test',status:'timed_out',snippet:'killed after 150s'}),/timed out — killed after 150s/);
assert.match(trailText({id:4,kind:'run',createdAt:'',event:'started',task:'整理下載資料'}),/整理下載資料/);
assert.match(trailText({id:5,kind:'run',createdAt:'',event:'completed',turns:9}),/Done in 9 turns/);
assert.match(trailText({id:6,kind:'run',createdAt:'',event:'waiting_input',reason:'登入'}),/Waiting for you: 登入/);
assert.match(trailText({id:10,kind:'run',createdAt:'',event:'paused',reason:'已達本輪執行上限'}),/Paused: 已達本輪執行上限/);
assert.match(trailText({id:7,kind:'run',createdAt:'',event:'retry'}),/Re-queued/);
assert.match(trailText({id:8,kind:'retry',createdAt:'',attempt:2,gaveUp:true,error:'429 rate limit'}),/attempt 2 failed, retrying · gave up — 429 rate limit/);
assert.match(trailText({id:9,kind:'notice',createdAt:'',text:'這輪不需要電腦'}),/這輪不需要電腦/);
});
test('meeting mode is a desktop layout switch with a toggle in the title bar',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/meeting-mode/);
assert.match(app,/meeting-toggle/);
assert.match(app,/function toggleMeeting/);
assert.match(app,/className=\{`meeting-stage/);
const css=fs.readFileSync('apps/web/src/responsive.css','utf8');
assert.match(css,/meeting-toggle\{display:none!important\}/);
assert.match(css,/\.meeting-stage\{display:none!important\}/);
assert.match(app,/part==="computer"&&isPhoneLayout\(\)/);
assert.match(app,/createPortal/);
const computerCss=fs.readFileSync('apps/web/src/computer.css','utf8');
assert.match(computerCss,/\.meeting-stage\{display:none!important\}/);
});
test('chat messages use a copy-or-reply menu instead of inline run details',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/function MessageContextMenu/);
assert.match(app,/t\("replyToThis"\)/);
assert.match(app,/t\("copyMessage"\)/);
assert.match(app,/\{\.\.\.messagePress\(message\)\}/);
assert.match(app,/if\(message\.role==="user"\)return/);
assert.match(app,/canReply=context\.message\.role!=="user"/);
assert.match(app,/kind:"reply"/);
assert.doesNotMatch(app,/CopyMessageButton/);
assert.doesNotMatch(app,/remember-msg/);
assert.doesNotMatch(app,/className="route-note"/);
});
test('the bubble reads the run activity endpoint the API actually mounts',()=>{
const monitor=fs.readFileSync('crates/api/src/monitor.rs','utf8');
assert.match(monitor,/\.route\("\/api\/runs\/\{id\}\/activity", get\(activity\)\)/);
assert.match(monitor,/\.route\("\/api\/runs\/\{id\}\/retry", post\(retry\)\)/);
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/chip\.kind==="error"\?/);
assert.match(app,/errorActions\(chip\.code\)/);
assert.match(app,/`\/api\/runs\/\$\{runId\}\/retry`,\{method:"POST",body:"\{\}"\}/);
assert.match(app,/<RunProbe runId=\{chip\.runId\|\|null\} align="end" label=\{t\("errorDetails"\)\}/);
assert.match(app,/<RunProbe runId=\{member\.id===computer\.botId\?computer\.busyRunId:null\}><Avatar/);
assert.match(app,/<RunProbe runId=\{computer\.busyRunId\|\|computer\.waitingRunId\}>/);
assert.doesNotMatch(app,/message-run-details/);
assert.match(app,/className="thinking-row"/);
const probe=fs.readFileSync('apps/web/src/run-monitor.tsx','utf8');
assert.match(probe,/`\/api\/runs\/\$\{runId\}\/activity\$\{after\}`/);
assert.doesNotMatch(probe,/export function RunStatus/);
assert.match(fs.readFileSync('apps/web/src/main.tsx','utf8'),/import "\.\/monitor\.css";/);
});
const liveJs=ts.transpileModule(fs.readFileSync('apps/web/src/live.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const liveBox={exports:{}};vm.runInNewContext(liveJs,liveBox);
const {sessionEventsUrl,subscribeToSession,SESSION_EVENT_TYPES,createCoalescer}=liveBox.exports;
test('the browser listens for every event kind the api can append',()=>{
const rust=['crates/api/src/runs.rs','crates/api/src/sessions.rs','crates/api/src/schedules.rs','crates/api/src/voice_call.rs'].map(file=>fs.readFileSync(file,'utf8')).join('\n');
const emitted=[...rust.matchAll(/["']((?:message|run|session)\.[a-z_]+)["']/g)].map(match=>match[1]);
assert.ok(emitted.length>=4,'the api should emit session events');
for(const kind of new Set(emitted))assert.ok(SESSION_EVENT_TYPES.includes(kind),`${kind} is emitted but never listened for`);
});
test('the live feed reads its own session, drops replays, and survives a bad frame',()=>{
const listeners={};let closed=0,url='';
const source={addEventListener:(type,listener)=>{listeners[type]=listener},close:()=>{closed+=1}};
const seen=[],statuses=[];
const feed=subscribeToSession('s/1',event=>seen.push(event),{source:candidate=>{url=candidate;return source},onStatus:connected=>statuses.push(connected)});
assert.equal(url,'/api/sessions/s%2F1/events');
assert.ok(listeners['message.created']&&listeners['run.completed']&&listeners['session.cleared'],'every kind gets a listener');
listeners['message.created']({lastEventId:'5',data:'{"seq":5,"role":"assistant"}'});
// The event object was built inside a vm realm, so compare fields, not prototypes.
assert.equal(seen.length,1);
assert.equal(seen[0].kind,'message.created');assert.equal(seen[0].id,5);
assert.equal(seen[0].payload.seq,5);assert.equal(seen[0].payload.role,'assistant');
listeners['message.created']({lastEventId:'5',data:'{}'});
listeners['message.created']({lastEventId:'4',data:'{}'});
assert.equal(seen.length,1,'a reconnect replay must not be applied twice');
listeners['run.started']({lastEventId:'6',data:'not json'});
assert.equal(seen.length,2);assert.equal(seen[1].kind,'run.started');assert.equal(seen[1].id,6);
assert.equal(Object.keys(seen[1].payload).length,0,'a malformed frame still reports the event');
listeners['open']();listeners['error']();
assert.deepEqual(statuses,[true,false]);
feed.close();feed.close();
assert.equal(closed,1,'close is idempotent');
assert.equal(sessionEventsUrl('abc'),'/api/sessions/abc/events');
});
test('a burst of session events settles into one refresh',()=>{
const timers=[];let runs=0;
const schedule=(callback,ms)=>{timers.push({callback,ms});return timers.length-1};
const dismiss=handle=>{if(timers[handle])timers[handle].cancelled=true};
const coalescer=createCoalescer(()=>{runs+=1},120,schedule,dismiss);
coalescer.kick();coalescer.kick();coalescer.kick();
assert.equal(runs,0);
assert.equal(timers.filter(entry=>!entry.cancelled).length,1,'one pending run');
assert.equal(timers[0].ms,120);
timers[0].callback();
assert.equal(runs,1);
coalescer.kick();coalescer.cancel();coalescer.cancel();
assert.equal(timers[1].cancelled,true);
assert.equal(runs,1,'a cancelled run never fires');
coalescer.kick();timers[2].callback();
assert.equal(runs,2,'the coalescer is reusable after a cancel');
});
test('chat follows the event stream instead of a fixed two second poll',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/subscribeToSession\(activeSessionId,event=>\{[^}]*settle\.kick\(\)\}/,'every non-streaming event kicks the settle coalescer');
assert.match(app,/createCoalescer\(\(\)=>\{if\(!document\.hidden\)refresh\(\)\.catch\(\(\)=>\{\}\)\},EVENT_SETTLE_MS\)/);
assert.match(app,/document\.addEventListener\("visibilitychange",resume\)/);
assert.doesNotMatch(app,/const timer=setInterval\(\(\)=>\{refresh\(\)/,'the 2s transcript poll should be gone');
assert.match(app,/const heartbeat=window\.setInterval\(\(\)=>\{const beat=roomsRef\.current[\s\S]*\},HEARTBEAT_MS\)/,'the heartbeat keeps its own minute cadence');
});
// The screen veil and the frame that survives it: both used to be driven by a
// timer plus "null the url", which is what made booting and handing over feel
// like a stall instead of a gesture.
const handoffJs=ts.transpileModule(fs.readFileSync('apps/web/src/handoff.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const handoffBox={exports:{},require:()=>({})};vm.runInNewContext(handoffJs,handoffBox);
const {viewerPath,keepScreenUrl,handoffRemaining,nextVeil,viewOnlyFor,HANDOFF_MS,HANDOFF_MIN_MS,VEIL_FADE_MS}=handoffBox.exports;
test('the viewer path is stable, so a frame can mount before the desktop answers',()=>{
assert.equal(viewerPath('bot 7'),'/view/bot%207/vnc.html');
assert.equal(viewerPath('a/b'),'/view/a%2Fb/vnc.html');
assert.equal(viewerPath('bot'),viewerPath('bot'),'no nonce, or the frame would remount every poll');
});
test('the path the frame mounts on is the byte the api hands back',()=>{
// The viewer mounts on the client's own path while the desktop is coming up,
// then adopts the url from the status round trip. If the two ever differ the
// iframe remounts mid-boot, which is the black flash this whole path exists
// to avoid, so the invariant is pinned to the server's own format string.
const routes=fs.readFileSync('crates/api/src/routes.rs','utf8');
const served=(routes.match(/"url": format!\("([^"]+)"\)/)||[])[1];
assert.equal(served,'/view/{id}/vnc.html');
const id='6f1d3a0e-2b7c-4d5e-8f90-1a2b3c4d5e6f';
assert.equal(viewerPath(id),served.replace('{id}',id));
});
test('pixels are dropped only for a computer that is really gone',()=>{
const url='/view/bot/vnc.html';
for(const state of ['booting','suspended','running'])assert.equal(keepScreenUrl(url,null,state),url,state+' must keep its VNC session');
for(const state of ['stopped','error'])assert.equal(keepScreenUrl(url,null,state),null,state+' has no desktop to keep');
assert.equal(keepScreenUrl(null,null,'running'),null,'nothing was ever mounted');
assert.equal(keepScreenUrl(url,'/other','booting'),'/other','a fresh url still wins');
assert.equal(keepScreenUrl(null,url,'booting'),url,'the first url arrives while booting');
});
test('a handoff holds one visible beat, never the whole round trip',()=>{
assert.ok(HANDOFF_MIN_MS<HANDOFF_MS,'the beat needs a floor and a ceiling');
assert.ok(VEIL_FADE_MS<=HANDOFF_MIN_MS,'the fade has to finish inside the beat');
assert.ok(HANDOFF_MS<=1_000,'a reply that never lands cannot strand the mascot');
assert.equal(handoffRemaining(1_000,1_000),HANDOFF_MIN_MS,'a reply this instant still owes the beat');
assert.equal(handoffRemaining(1_000,1_000+HANDOFF_MIN_MS),0,'served');
assert.equal(handoffRemaining(1_000,1_000+(HANDOFF_MIN_MS+HANDOFF_MS)/2),0,'past the floor is never a wait');
assert.equal(handoffRemaining(1_000,1_000+HANDOFF_MS),0);
const midway=handoffRemaining(1_000,1_000+HANDOFF_MIN_MS-10);
assert.equal(midway,10,'the remaining beat shrinks with the clock');
});
test('the veil fades in with its label and back out through the same mascot',()=>{
const empty={label:null,leaving:false};
assert.equal(nextVeil(null,empty),empty,'with nothing to say there is nothing to show');
const shown=nextVeil('換手中',empty);
assert.equal(shown.label,'換手中');assert.equal(shown.leaving,false);
assert.equal(nextVeil('換手中',shown),shown,'a repeated status must not restart the animation');
const leaving=nextVeil(null,shown);
assert.equal(leaving.label,'換手中','the label is held while it fades');assert.equal(leaving.leaving,true);
assert.equal(nextVeil(null,leaving),leaving,'it leaves exactly once');
const replaced=nextVeil('喚醒中',shown);
assert.equal(replaced.label,'喚醒中');assert.equal(replaced.leaving,false,'a new status replaces the label');
const revived=nextVeil('喚醒中',leaving);
assert.equal(revived.label,'喚醒中');assert.equal(revived.leaving,false,'and pulls it back out of the fade');
});
test('only a human moves the mouse, and the veil never holds it back',()=>{
assert.equal(viewOnlyFor('user'),false);
for(const holder of ['none','bot'])assert.equal(viewOnlyFor(holder),true,holder);
});
test('shared input stays interactive across agent ownership and pause states',()=>{
for(const holder of ['none','bot','user'])assert.equal(viewOnlyFor(holder,true),false,holder);
});
test('taking the screen flips the mouse on the click, not on the reply',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
const body=app.slice(app.indexOf('async function setControl('));
const flip=body.indexOf('controlHolder:holder');
const gate=body.indexOf('pushViewOnly(viewOnlyFor(holder,computer.sharedInput))');
const post=body.indexOf('await api(`/api/computer/${botId}/${holder==="user"?"takeover":"release"}`');
const readBack=body.indexOf('finally{');
assert.ok(flip>0&&gate>0&&post>0&&readBack>0,'optimistic flip, gate, request, and read back all present');
assert.ok(flip<post&&gate<post,'badge and input move before the server answers');
assert.ok(readBack>post,'the truth is read back after the request');
assert.match(body.slice(readBack),/await refresh\(\)/);
assert.match(body,/if\(!botId\|\|controlBusyRef\.current\)return/,'a double click cannot fight itself');
assert.match(app,/if\(computer\.sharedInput\)\{pushViewOnly\(!desktopInteractive\);return\}void setControl\("user"\)/);
assert.match(app,/if\(expectedHolderRef\.current&&status\.controlHolder===expectedHolderRef\.current\)/,'agreement, not a timer, ends the handoff');
assert.match(app,/onClick=\{onTakeOver\}/);
assert.match(app,/onClick=\{onRelease\}/);
assert.doesNotMatch(app,/action\(\(\)=>api\(`\/api\/computer\/[^`]*takeover/,'takeover no longer rides the global busy path');
assert.equal((app.match(/\/api\/computer\/\$\{[^}]*\}\/(takeover|release)/g)||[]).length,0,'every handoff goes through setControl');
assert.equal((app.match(/holder==="user"\?"takeover":"release"/g)||[]).length,1,'one request path, one owner of it');
});
test('shared CUA desktops hide takeover and keep the mouse live',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/sharedInput:true/,'the first paint already assumes shared input');
assert.match(app,/if\(!computer\.sharedInput\)await setControl\("user",id\)/,'opening the login screen must not pause a shared run');
assert.match(app,/if\(active&&!computer\.sharedInput\)void setControl\("user",active\.id\)/,'a call only takes exclusive control on older exclusive desktops');
assert.match(app,/computer\.sharedInput\?\(computer\.takeoverRequested\|\|computer\.controlHolder==="user"\?<button className="primary" disabled=\{busy\|\|pending\} onClick=\{onRelease\}>\{t\("doneContinue"\)\}<\/button>:null\)/);
assert.match(app,/computer\.sharedInput\?t\("sharedDesktop"\)/);
assert.match(app,/computer\.sharedInput\?t\("sharedNeedsUser"/);
assert.match(app,/takeover=\{computer\.takeoverRequested\|\|\(!computer\.sharedInput&&computer\.controlHolder==="user"\)\}/);
const vnc=fs.readFileSync('apps/web/vnc.html','utf8');
assert.match(vnc,/rfb\.viewOnly = true;/,'a dropped RFB is muted locally');
assert.doesNotMatch(vnc,/disconnect[\s\S]{0,400}applyViewOnly\(true\)/,'disconnect must not overwrite host intent');
});
test('the waiting veil covers the desktop the way it always did',()=>{
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
assert.match(app,/<div className="preview">\{computerOpen\?<EmptyComputer state=\{computer\.state\}\/>:frame\}\{!computerOpen&&hud\}<\/div>/);
assert.match(app,/<div className="overlay-desktop">\{frame\}\{hud\}<\/div>/);
assert.match(app,/const hud=paneBot&&veil\.label\?<ComputerHud bot=\{paneBot\} label=\{veil\.label\} leaving=\{veil\.leaving\}\/>:null/);
const css=fs.readFileSync('apps/web/src/computer.css','utf8');
// Both parents are what make inset:0 cover the desktop rather than the page.
assert.match(css,/\.computer-part \.preview\{position:relative/);
assert.match(css,/\.overlay-screen \.overlay-desktop\{position:relative/);
// These values were compared against main in a real browser: rendered pixel
// for pixel identical, so drift here means the veil visibly changed.
const hud=css.match(/^\.computer-hud\{([^}]*)\}/m)[1];
for(const rule of ['position:absolute','inset:0','z-index:3','display:grid','align-content:center','justify-items:center','gap:14px','padding:10px 12px 8px','border-radius:inherit','background:radial-gradient(ellipse at center,#142722e8,#101012ed)','backdrop-filter:blur(8px)','pointer-events:none'])assert.ok(hud.includes(rule),`the veil needs ${rule}`);
assert.match(css,/\.computer-hud\.is-leaving\{opacity:0\}/);
assert.match(css,/\.computer-hud\{[^}]*transition:opacity \.22s ease\}/,'it fades instead of blinking off');
// main drew the label as static mint text; the shimmer belongs to the chat
// working label, and the veil keeps the mascot as the only moving part.
const label=css.match(/^\.computer-hud-label\{([^}]*)\}/m)[1];
assert.ok(!label.includes('animation'),'the veil label does not animate');
assert.ok(label.includes('color:#c9ddd5')&&label.includes('font-size:12px')&&label.includes('text-align:center')&&label.includes('max-width:90%'));
assert.ok(!/\.computer-hud[^{]*\{[^}]*working-shimmer/.test(css),'nothing shimmers inside the veil');
assert.match(css,/\.computer-signal\{[^}]*width:64px[^}]*animation:monitor-breathe/);
assert.match(css,/\.computer-signal::before\{[^}]*monitor-orbit/);
const responsive=fs.readFileSync('apps/web/src/responsive.css','utf8');
assert.match(responsive,/prefers-reduced-motion:reduce\)\{\.computer-signal,\.computer-signal::before,\.computer-signal-face i[^}]*animation:none/,'reduced motion still has to still the mascot');
});
test('a desktop that is still starting is dialled again fast, a dropped session calmly',()=>{
const source=fs.readFileSync('apps/web/vnc.html','utf8').match(/<script type="module">([\s\S]*?)<\/script>/)[1].replace(/import RFB[^;]+;/,'');
// Any element the viewer touches answers, so the disconnect path runs the
// same way it does in a browser without a DOM to stand in.
const element=()=>new Proxy({style:{},classList:{toggle(){}},dataset:{},hidden:false,textContent:''},{get:(node,key)=>key in node?node[key]:()=>undefined,set:(node,key,value)=>{node[key]=value;return true}});
const instances=[],scheduled=[];
class RFB{constructor(){this._handlers={};instances.push(this._handlers);}
addEventListener(name,handler){(this._handlers[name]||(this._handlers[name]=[])).push(handler);} focus(){} blur(){} sendKey(){}}
const window={location:{pathname:'/vnc.html',protocol:'http:',host:'localhost',origin:'http://localhost',hash:''},parent:{postMessage(){}},addEventListener(){}};
vm.runInNewContext(source,{window,document:{location:{href:'http://localhost/vnc.html?view_only=false'},getElementById:()=>element(),querySelector:()=>null},navigator:{clipboard:{}},RFB,setTimeout:(fn,ms)=>{scheduled.push(ms);return scheduled.length},clearTimeout(){}});
const fire=(name,event)=>{for(const handlers of instances)(handlers[name]||[]).forEach(handler=>handler(event||{}));};
assert.equal(instances.length,1,'the viewer dials once on load');
fire('disconnect',{clean:false});
assert.equal(scheduled.at(-1),350,'a desktop that has never answered is retried eagerly');
fire('connect');
fire('disconnect',{clean:true});
assert.equal(scheduled.at(-1),1500,'a session that was really there is retried calmly');
});
for(const hostUpdatesDuringReconnect of [false,true])test(`reconnect preserves shared input (host update during reconnect: ${hostUpdatesDuringReconnect})`,()=>{
const vnc=fs.readFileSync('apps/web/vnc.html','utf8');
// Run the viewer rather than read it. The host hands the mouse over while the
// viewer sits between two sessions: exactly the moment a fresh RFB used to
// fall back to its view-only default and leave a taken-over desktop deaf with
// the veil already lifted. Only the gate message arriving is real here.
const body=vnc.match(/<script type="module">([\s\S]*?)<\/script>/)[1].replace(/import RFB[^;]+;/,'');
const element=()=>new Proxy({style:{},classList:{toggle(){}},dataset:{},hidden:false,textContent:''},{get:(node,key)=>key in node?node[key]:()=>undefined,set:(node,key,value)=>{node[key]=value;return true}});
const instances=[],scheduled=[],messages=[];
class RFB{constructor(){this._handlers={};instances.push(this);}addEventListener(name,handler){(this._handlers[name]||(this._handlers[name]=[])).push(handler);}focus(){this.focused=true;}blur(){}}
const parent={postMessage(){}};
const window={location:{pathname:'/vnc.html',protocol:'http:',host:'localhost',origin:'http://localhost',hash:''},parent,addEventListener(name,handler){if(name==='message')messages.push(handler);}};
vm.runInNewContext(body,{window,document:{location:{href:'http://localhost/vnc.html'},getElementById:()=>element(),querySelector:()=>null},navigator:{clipboard:{}},RFB,setTimeout:(fn)=>{scheduled.push(fn);return scheduled.length},clearTimeout(){}});
const fire=(name,event)=>instances.forEach(rfb=>(rfb._handlers[name]||[]).forEach(handler=>handler(event||{})));
assert.ok(messages.length,'the viewer listens for the host gate');
const allowInput=()=>messages.forEach(handler=>handler({origin:'http://localhost',source:parent,data:{type:'lazyboy-view-only',viewOnly:false}}));
allowInput();
fire('disconnect',{clean:false});
if(hostUpdatesDuringReconnect)allowInput();
scheduled.at(-1)();
fire('connect');
assert.equal(instances.length,2,'the retry dialed a new session');
assert.equal(instances.at(-1).viewOnly,false,'a handoff made mid-reconnect still owns the new session');
assert.equal(instances.at(-1).focused,true,'taking over lands the first keystroke');
});
// Who speaks in a room is decided by three small pure functions, so the `@`
// list is run rather than read: which token the caret is writing, who that token
// could mean, and what the composer holds after a pick.
const mentionsJs=ts.transpileModule(fs.readFileSync('apps/web/src/mentions.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const mentionsBox={exports:{},require:()=>{throw new Error('unexpected import')}};
vm.runInNewContext(mentionsJs,mentionsBox);
const {mentionToken,mentionChoices,acceptMention,ensureMention,replySnippet,mentionSpans,nameHit}=mentionsBox.exports;
const EVERYONE='所有人';
const roster=[{id:'b1',name:'小美'},{id:'b2',name:'Alex'},{id:'b3',name:'Alexa'},{id:'b4',name:'工程小助手'}];
// The list is built inside a vm, so its arrays are not this realm's arrays:
// compare what a person sees (names, joined) rather than the objects themselves.
const names=(token,source=roster)=>mentionChoices(token,source,EVERYONE).map(choice=>choice.name).join(',');
test('a mention list opens only on the token under the caret',()=>{
assert.equal(mentionToken('@',1),'','a bare @ is still a list, not a word');
assert.equal(mentionToken('@小美',3),'小美');
assert.equal(mentionToken('嗨 @小美 @',7),'','the last word wins, the earlier mention is spent');
assert.equal(mentionToken('@小美 幫我看',4),null,'a finished name closes the list');
assert.equal(mentionToken('幫我改一下佈景',6),null,'plain text offers nobody');
assert.equal(mentionToken('mail@example.com',16),null,'an email address is not a mention');
assert.equal(mentionToken('@@',2),null,'a doubled @ is a typo, not a name');
});
test('the whole-room escape hatch is the first name you are offered',()=>{
const open=mentionChoices('',roster,EVERYONE);
assert.equal(open[0].name,EVERYONE,'@ comes with the escape hatch on top');
assert.equal(open[0].member,null,'it reaches the room, so it is nobody in particular');
assert.equal(names('').split(',').length,roster.length+1,'then the whole room, in order');
assert.equal(names('').split(',')[1],roster[0].name);
assert.equal(names('所'),EVERYONE,'a token that cannot grow into 所有人 drops it');
assert.equal(names(EVERYONE).split(',')[0],EVERYONE,'typing it out keeps it offered');
});
test('mention choices match loosely and stay one glance long',()=>{
assert.equal(names('alex'),'Alex,Alexa','case does not matter, room order does');
assert.equal(names('小'),'小美,工程小助手','a name anywhere in the word hits');
assert.equal(names(' nobody '),'','a token nobody can answer to offers nobody');
const crowd=Array.from({length:9},(_,index)=>({id:`b${index}`,name:`bot${index}`}));
assert.equal(names('',crowd).split(',').length,6,'six rows is the cap');
assert.equal(names('',crowd).split(',')[0],EVERYONE,'and the escape hatch still leads');
});
test('accepting a mention leaves the sentence readable and the list closed',()=>{
const fresh=acceptMention('@',1,'小美');
assert.equal(fresh.text,'@小美 ');
assert.equal(fresh.caret,fresh.text.length,'the caret sits past the space, so typing continues the sentence');
assert.equal(mentionToken(fresh.text,fresh.caret),null,'and the list does not reopen behind it');
const picked=acceptMention('幫我 @小 部署',5,'小美');
assert.ok(picked.text.startsWith('幫我 @小美 '),'only the typed token is replaced');
assert.ok(picked.text.includes('部署'),'the rest of the message survives');
assert.equal(acceptMention('@小',2,'小美').text,'@小美 ','a half-typed name is replaced whole');
});
test('replying to a room message names the speaker once',()=>{
assert.equal(ensureMention('','小美'),'@小美 ');
assert.equal(ensureMention('看一下這個','小美'),'@小美 看一下這個');
assert.equal(ensureMention('@小美 已經點名','小美'),'@小美 已經點名','an existing mention is left alone');
assert.equal(ensureMention('請 @小美 幫忙','小美'),'請 @小美 幫忙','a mention later in the sentence also counts');
assert.equal(ensureMention(' 前面有空白','Alex'),'@Alex 前面有空白');
assert.equal(ensureMention('',''),'');
assert.equal(replySnippet('沒拚嘛,在這等你聊天~有事直接說就好。'),'沒拚嘛,在這等你聊天~有事直接說就好。');
assert.equal(replySnippet('one\n\ntwo three'),'one two three');
const long='字'.repeat(90);
assert.equal(replySnippet(long).length,80);
assert.ok(replySnippet(long).endsWith('…'));
});
test('group @mentions become agent spans and ignore emails',()=>{
const room=[{id:'b',name:'小美'},{id:'a',name:'阿明'},{id:'c',name:'Dev'}];
const kinds=(text)=>mentionSpans(text,room).map(span=>span.kind==='agent'?`${span.kind}:${span.id}`:`${span.kind}:${span.text}`).join('|');
assert.equal(kinds('@小美 幫我看這份報表'),'agent:b|text: 幫我看這份報表');
assert.equal(kinds('@阿明哥 來一下'),'agent:a|text:哥 來一下','a CJK name has no word boundary, so 阿明 matches inside 阿明哥 when that longer member is absent');
const overlap=[{id:'a',name:'阿明'},{id:'b',name:'阿明哥'}];
assert.equal(mentionSpans('@阿明哥 來一下',overlap)[0].id,'b');
assert.equal(mentionSpans('@阿明 來一下',overlap)[0].id,'a');
assert.equal(kinds('@dev 幫我看'),'agent:c|text: 幫我看');
assert.equal(kinds('@Dev, help'),'agent:c|text:, help');
assert.equal(kinds('@Developer 幫我看'),'text:@Developer 幫我看');
assert.equal(kinds('寄到 mail@小美 或 report@dev.example 就好'),'text:寄到 mail@小美 或 report@dev.example 就好');
assert.equal(kinds('@小美)'),'text:|agent:b|text:');
assert.equal(mentionSpans('@所有人 結論定了',room)[0].kind,'everyone');
assert.equal(mentionSpans('@everyone look',room)[0].kind,'everyone');
assert.ok(nameHit('小美 幫我','小美'));
assert.equal(mentionSpans('@allison 來了',room)[0].kind,'text');
assert.equal(mentionSpans('@小美 幫我看',[])[0].kind,'text','one-to-one chats do not link mentions');
});
// When a message was said is split the way chat apps split it: the day once,
// as a divider, and the clock time on each message. The words for today and
// yesterday come from the catalog; everything else is the locale's own spelling.
const chatTimeJs=ts.transpileModule(fs.readFileSync('apps/web/src/chat-time.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const chatTimeBox={exports:{},require:()=>{throw new Error('unexpected import')}};
vm.runInNewContext(chatTimeJs,chatTimeBox);
const {dayLabel,sameDay,clockTime}=chatTimeBox.exports;
const words={today:zhTW.today,yesterday:zhTW.yesterday};
// A Wednesday afternoon, built in local time so the calendar day is unambiguous.
const now=new Date(2026,9,7,15,30);
const daysAgo=(count,hour=9)=>new Date(2026,9,7-count,hour);
test('a day is named the way people say it',()=>{
assert.equal(dayLabel(daysAgo(0,0),now,'zh-TW',words),'今天','midnight still counts as today');
assert.equal(dayLabel(daysAgo(0,23),now,'zh-TW',words),'今天','a message later today is still today, not the future');
assert.equal(dayLabel(daysAgo(1,23),now,'zh-TW',words),'昨天','yesterday means the calendar day, not 24 hours');
assert.equal(dayLabel(daysAgo(1),now,'en',{today:en.today,yesterday:en.yesterday}),'Yesterday');
assert.equal(dayLabel(daysAgo(2),now,'zh-TW',words),'星期一','the past week goes by weekday');
assert.equal(dayLabel(daysAgo(6),now,'zh-TW',words),'星期四','six days back is the last weekday');
const week=dayLabel(daysAgo(7),now,'zh-TW',words);
assert.match(week,/9\/30|9月30日/,'a week or more ago is written as a date');
assert.match(week,/週三|星期三/,'with its weekday, so it reads like a chat and not a form');
assert.ok(!/2026/.test(week),'this year is not spelled out');
assert.match(dayLabel(new Date(2025,9,1),now,'zh-TW',words),/2025/,'another year is spelled out');
assert.match(dayLabel(new Date(2026,8,15),now,'en',{today:en.today,yesterday:en.yesterday}),/Tue, 9\/15/,'English keeps the same short numeric date');
assert.equal(dayLabel('not a date',now,'zh-TW',words),'','garbage shows nothing rather than Invalid Date');
});
test('messages split by calendar day and show only their clock time',()=>{
assert.ok(sameDay(new Date(2026,9,7,0,1),new Date(2026,9,7,23,59)),'first and last minute share a day');
assert.ok(!sameDay(new Date(2026,9,7,23,59),new Date(2026,9,8,0,0)),'a minute past midnight is a new day');
assert.ok(!sameDay('garbage','garbage'),'two unreadable dates are not the same day');
assert.equal(clockTime(new Date(2026,9,7,9,5),'zh-TW'),'09:05','24-hour clock, zero padded');
assert.equal(clockTime(new Date(2026,9,7,15,30),'en'),'15:30');
assert.equal(clockTime('garbage','en'),'');
});
const keyJs=ts.transpileModule(fs.readFileSync('apps/web/src/model-key.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const keyBox={exports:{},require:()=>{throw new Error('unexpected import')}};
vm.runInNewContext(keyJs,keyBox);
const {STORED_API_KEY_MASK,storedApiKeyFieldValue,applyApiKeyInput,apiKeySavePayload,providerHasStoredKey,emptyProviderDraft}=keyBox.exports;
test('a stored API key is shown as dots and never sent back',()=>{
const stored=emptyProviderDraft();
assert.equal(storedApiKeyFieldValue(true,stored),STORED_API_KEY_MASK);
const keep=apiKeySavePayload(stored);
assert.equal(keep.apiKey,null);
assert.equal(keep.clearApiKey,false);
const typed=applyApiKeyInput(true,STORED_API_KEY_MASK+'sk-new');
assert.equal(typed.apiKey,'sk-new');
assert.equal(typed.keyEdited,true);
const replaced=apiKeySavePayload(typed);
assert.equal(replaced.apiKey,'sk-new');
assert.equal(replaced.clearApiKey,false);
const cleared=apiKeySavePayload({...stored,clearKey:true});
assert.equal(cleared.apiKey,'');
assert.equal(cleared.clearApiKey,true);
const masked=apiKeySavePayload({apiKey:STORED_API_KEY_MASK,keyEdited:true,clearKey:false});
assert.equal(masked.apiKey,null);
assert.equal(masked.clearApiKey,false);
});
test('each provider remembers whether it already has a key',()=>{
const settings={apiKeySet:true,provider:'xai',providers:[
{id:'xai',apiKeySet:true},
{id:'opencode-go',apiKeySet:false},
]};
assert.equal(providerHasStoredKey(settings,'xai'),true);
assert.equal(providerHasStoredKey(settings,'opencode-go'),false);
assert.equal(providerHasStoredKey({apiKeySet:true,provider:'xai'},'xai'),true);
assert.equal(providerHasStoredKey({apiKeySet:true,provider:'xai'},'opencode-go'),false);
});
test('file evidence button uses readback API and cancels work when its panel closes',async()=>{
const calls=[];const updates=[];let cleanup;let complete;
const hooks={useCallback:fn=>fn,useRef:()=>({current:null}),useEffect:fn=>{cleanup=fn()},useState:initial=>[initial,value=>updates.push(value)]};
const source=ts.transpileModule(fs.readFileSync('apps/web/src/run-monitor.tsx','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const runtime={exports:{},AbortController,require:name=>{
if(name==='react')return hooks;
if(name==='react/jsx-runtime')return{jsx:(type,props)=>({type,props}),jsxs:(type,props)=>({type,props}),Fragment:'Fragment'};
if(name==='./i18n')return i18n;
if(name==='./api')return{api:(path,options)=>{calls.push({path,options});return new Promise(resolve=>{complete=resolve})}};
throw new Error(name);
}};
vm.runInNewContext(source+'\nexports.evidenceComponent=FileOperationEvidence;',runtime);
const tree=runtime.exports.evidenceComponent({botId:'bot',operationId:'original'});
let stopped=0;tree.props.onKeyDown({key:'Enter',stopPropagation:()=>stopped++});assert.equal(stopped,1);
const button=tree.props.children[0];button.props.onClick({stopPropagation:()=>stopped++});
assert.equal(calls.length,1);assert.equal(calls[0].path,'/api/bots/bot/file-operation');
assert.equal(calls[0].options.method,'POST');assert.deepEqual(JSON.parse(calls[0].options.body),{operationId:'original',readback:true});
cleanup();assert.equal(calls[0].options.signal.aborted,true);
const before=updates.length;complete({operationId:'original',status:'unknown',evidence:{desired_state_matches:true}});
await new Promise(resolve=>setImmediate(resolve));assert.equal(updates.length,before,'closed panel must ignore its late response');
let hookIndex=0;
const evidence={desired_state_matches:true,helper_quiescent:true,controller_quiescent:true};
hooks.useState=initial=>[hookIndex++===0?{operationId:'original',status:'unknown',evidence}:initial,value=>updates.push(value)];
const eligible=runtime.exports.evidenceComponent({botId:'bot',operationId:'original'});
eligible.props.children[1].props.onClick({stopPropagation:()=>{}});
assert.deepEqual(JSON.parse(calls[1].options.body),{operationId:'original',readback:true,resolve:true});
complete({operationId:'original',status:'succeeded',evidence:{verified:true,verificationBasis:'postcondition'}});
await new Promise(resolve=>setImmediate(resolve));
hookIndex=0;evidence.controller_quiescent=false;
const liveController=runtime.exports.evidenceComponent({botId:'bot',operationId:'original'});
assert.equal(liveController.props.children[1],false,'live controller must not offer resolution');
hookIndex=0;
hooks.useState=initial=>[hookIndex++===0?{operationId:'original',status:'unknown',historical:true,retirementRecoveryAvailable:true}:initial,value=>updates.push(value)];
const count=calls.length;
const historical=runtime.exports.evidenceComponent({botId:'bot',operationId:'original'});
assert.equal(calls.length,count,'rendering an eligible operation must not trigger recovery');
historical.props.children[1].props.onClick({stopPropagation:()=>{}});
assert.equal(calls.length,count+1);
assert.deepEqual(JSON.parse(calls.at(-1).options.body),{operationId:'original',readback:true,resolve:true});
complete({operationId:'original',status:'unknown',historical:true,retirementRecoveryAvailable:false});
await new Promise(resolve=>setImmediate(resolve));
});