From 97d9280b6e080118147bc18fdb4f9b42940af970 Mon Sep 17 00:00:00 2001 From: i2cjak Date: Wed, 4 Nov 2026 15:27:49 -0500 Subject: [PATCH] test: bind desktop fixtures to the current host and renderer 23 tests in the small gate's Node batch fail on main, because these fixtures run slices of host.js or read the renderer and fell behind them. Load the real names the slices now reach, move an end marker that left host.js, reach the probe path #941 moved behind directReconnect, and re-bind the focused-cache shape checks to the static pending bubble. Adds one check that the bubble stays out of a saved picture. Tests only; nothing is deleted or skipped. --- desktop/test/focused-cache.test.cjs | 15 ++++++++++++--- desktop/test/host-thin-startup.test.cjs | 18 +++++++++++++----- desktop/test/thin-search-ui.test.cjs | 12 ++++++++---- desktop/test/thin-view-hold.test.cjs | 2 +- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/desktop/test/focused-cache.test.cjs b/desktop/test/focused-cache.test.cjs index eadfb9155f..dcdfc7e80d 100644 --- a/desktop/test/focused-cache.test.cjs +++ b/desktop/test/focused-cache.test.cjs @@ -70,15 +70,24 @@ test('cache restore never writes draft storage and a new local draft remains ind const {m,record}=await picture();const draft={text:'new unsent 日本語'};const view=new m.FocusedView({scope:()=>scope,cache:{get:async()=>record},hide(){},view(){}}); await view.restore();assert.equal(draft.text,'new unsent 日本語');assert.equal(Object.hasOwn(record,'drafts'),false);assert.equal(Object.hasOwn(record,'session'),false); }); -// This DOM shim follows the real normal live renderer: msg assistant live, -// body child, and NO data-id. A memo key is not a DOM data-id attribute. +// The web's own live row is a static pending bubble (View.pending.node, the +// product's static pending contract: docs/active-threads-load.md). This shim is +// the other stream row capture accepts: msg assistant live, body child, and NO +// data-id. A memo key is not a DOM data-id attribute. function normalLive(body) { return el('div',{class:'msg assistant live'},[el('div',{class:'body'},[text(body)])]); } test('normal live DOM shape matches the renderer rather than assuming a committed-message data-id',()=>{ const fs=require('node:fs'),path=require('node:path');const source=fs.readFileSync(path.resolve(__dirname,'../../src/web/view.bend'),'utf8'); const live=source.slice(source.indexOf('def View.live.n.loc('),source.indexOf('def View.live.n(',source.indexOf('def View.live.n.loc('))); - assert(live.includes('D.At{"class", "msg assistant live"}'));assert(live.includes('D.At{"class", "body"}'));assert(!live.includes('data-id')); + assert(live.includes('View.pending.node(lc, key)')); + const pending=source.slice(source.indexOf('def View.pending.node('),source.indexOf('def View.pending.rows(',source.indexOf('def View.pending.node('))); + assert(pending.includes('D.At{"class", "msg assistant pending"}'));assert(pending.includes('D.At{"class", "body"}'));assert(!pending.includes('data-id')); assert.equal(normalLive('x').hasAttribute('data-id'),false); }); +test('the static pending bubble the renderer draws stays out of a saved picture',async()=>{ + const {m,timeline}=await picture();timeline.children.push(el('div',{class:'msg assistant pending',role:'status'},[el('div',{class:'body'},[text('STATIC PENDING')])])); + const record=m.captureFocused({main:el('main'),timeline,title:el('span',{},[text('title')]),scope,epoch:'actual'}); + assert.equal(record.rows,1);assert(!JSON.stringify(record).includes('STATIC PENDING')); +}); test('a stream-only normal assistant row persists current partial Unicode as a readonly picture',async()=>{ const {m,timeline}=await picture();timeline.children=[normalLive('途中の返答 👩‍💻 é CURRENT')]; const record=m.captureFocused({main:el('main'),timeline,title:el('span',{},[text('Streaming thread')]),scope,epoch:'actual'}); diff --git a/desktop/test/host-thin-startup.test.cjs b/desktop/test/host-thin-startup.test.cjs index 585cd700a8..46ac6c4d2b 100644 --- a/desktop/test/host-thin-startup.test.cjs +++ b/desktop/test/host-thin-startup.test.cjs @@ -6,8 +6,10 @@ async function fixture(supported=false, legacyProbe=false, legacyPaint=false){ const {ViewerRpc}=await import('../../src/web/viewer-rpc.js'),{ReviewPosition}=await import('../../src/web/review-position.js'); const {ImprovementsRpc}=await import('../../src/web/improvements-rpc.js'); const {SubsFeedback}=await import('../../src/web/subs-feedback.js'); + const {SecretSaveHost}=await import('../../src/web/secret-save.js'); + const {phoneRequest}=await import('../../src/web/phone-shell.js'); const {EntryHistory}=await import('../../src/web/entry-history.js'); - const {ThinHost,presentationTarget,preservePresentationRoute,refusedFallback}=await import('../../src/web/thin-host.js'),{Backoff,Retry,probeHub,QUIET_MS}=await import('../../src/web/reconnect.js'),{ThinViews,viewCapability,VIEW_LIMITS}=await import('../../src/web/thin.js'),{discoverViews}=await import('../../src/web/thin-discovery.js'); + const {ThinHost,presentationTarget,preservePresentationRoute,refusedFallback}=await import('../../src/web/thin-host.js'),{Backoff,Retry,directReconnect,DIRECT_TRIES,probeHub,QUIET_MS}=await import('../../src/web/reconnect.js'),{ThinViews,viewCapability,VIEW_LIMITS}=await import('../../src/web/thin.js'),{discoverViews}=await import('../../src/web/thin-discovery.js'); let source=fs.readFileSync(path.resolve(__dirname,'../../src/web/host.js'),'utf8'); if(legacyProbe)source=source.replaceAll(' || attempt !== thinAttempt',''); if(legacyPaint)source=source.replace(' && !detail?.presenting','').replace('state.painted && !becameReady','state.paged && !becameReady'); @@ -31,15 +33,16 @@ async function fixture(supported=false, legacyProbe=false, legacyPaint=false){ presentationJson:x=>x,fromList:x=>x,presentationCommand:c=>calls.push(c),anchorOf:()=>null, History:{project:(_before,fresh)=>fresh,project_block:(_before,fresh)=>fresh},historyNode:null,timelineList:()=>null, App:{wire_out:t=>encoder.encode(t),wire_out_json:v=>encoder.encode(JSON.stringify(v)),viewer_input:(_u,_info,s)=>s,desk_reset:u=>u,presentation_node:n=>n,presentation_prepare:(s,input)=>({...input,rid:s.rid}),presentation_reply:s=>s,presentation_select:(u,id)=>({...u,sel:id}),presentation_handoff:(u,s)=>({...u,sel:s.sel}),unsync:u=>u,lang:()=> 'en'}, - Wire:{cbor:b=>JSON.parse(decoder.decode(b)),plain:x=>x,json:x=>x},document:{getElementById:()=>null,querySelector:()=>null,querySelectorAll:()=>[],documentElement:{classList:{toggle(){}},style:{setProperty(){}}}},pairingReady:Promise.resolve(true),pendingInvitation:null,pairingCause:'',pairingUI:{show(){calls.push('pairing-screen');}}, - fallbackIntent:null,wantFrontend:true,Backoff,Retry:FixtureRetry,probeHub,QUIET_MS,navigator:{onLine:true},thinContinuations:{clear(){},accept:()=>false,bind(){},project:x=>x},presentationTarget,preservePresentationRoute,checkDeploy:()=>{},outbox:new Map(),OUTBOX:'outbox',OUTBOX_DAY:86400000,DRAFT:'draft:', + Wire:{cbor:b=>JSON.parse(decoder.decode(b)),plain:x=>x,json:x=>x},document:{getElementById:()=>null,querySelector:()=>null,querySelectorAll:()=>[],documentElement:{classList:{toggle(){},contains:()=>false},style:{setProperty(){}}}},pairingReady:Promise.resolve(true),pendingInvitation:null,pairingCause:'',pairingUI:{show(){calls.push('pairing-screen');}}, + fallbackIntent:null,wantFrontend:true,Backoff,Retry:FixtureRetry,directReconnect,probeHub,QUIET_MS,navigator:{onLine:true},thinContinuations:{clear(){},accept:()=>false,bind(){},project:x=>x},presentationTarget,preservePresentationRoute,checkDeploy:()=>{},outbox:new Map(),OUTBOX:'outbox',OUTBOX_DAY:86400000,DRAFT:'draft:', fabLook(){},restoreTransition(){},transitionNode:null,transitionFor:'',shownFor:'',heldAt:0,transitionScope:()=>({}),transitionPosition:null,previewViews:{stop(){}},warmDestinations(){},transitionViews:{put(){},position:()=>null,warm:()=>Promise.resolve(false),destination:()=>null}, thinReading:null,resumedConversation:null,readingIdentity:()=> 'fixture-account',historyDirection:'', historyFillMore(){calls.push('history-fill-cleared');},historyPageState(){},historyClear(){},thinSearchAnswer(){},thinSearchFailed(){},showThinMessage(){},keepViewer(){},held:null, modelFeedback:{clear(){}},subsFeedback:new SubsFeedback(()=>{}),viewerFeedback:{choice(){}}, // The extracted socket adapter shares these services with the full host. // Record peripheral lifecycle calls; wire admission/ownership still uses real ThinHost/ThinViews. - RR:null,COARSE_POINTER:false,ViewerTools:{reset(){calls.push('viewer-tools-retired');}},viewerRpc:new ViewerRpc(()=>{throw Error('unexpected viewer RPC');}), + RR:null,COARSE_POINTER:false,PHONE:false,phoneRequest,ViewerTools:{reset(){calls.push('viewer-tools-retired');}},viewerRpc:new ViewerRpc(()=>{throw Error('unexpected viewer RPC');}), + secretSaves:new SecretSaveHost({build:()=>{throw Error('unexpected secret save');},scope:()=>'',binding:()=>'',send(){throw Error('unexpected secret send');},dispatch(){},note(){}}), improvementsRpc:new ImprovementsRpc(()=>{throw Error('unexpected Improvements RPC');}),voiceMicHeard(){calls.push('voice-envelope');}, reviewPosition:new ReviewPosition({getItem:k=>store.get(k)??null,setItem:(k,v)=>store.set(k,v)}), composerKeeper:{selecting(){},navigationFailed(){}},keepDesk:s=>s,deskCheck(){},settingsRecovery:null,settingsRecovering:null, @@ -55,8 +58,11 @@ async function fixture(supported=false, legacyProbe=false, legacyPaint=false){ for(const [start,end] of [['function keepThinReading(','// (the re-render harness'],['function keepSettingsRecovery(','async function recoverSettings('],['function restoreDraftAttachments(','function uploadScope(']]){ const from=source.indexOf(start),to=source.indexOf(end,from);assert(from>0&&to>from);vm.runInContext(source.slice(from,to),ctx); } + // Top-level names of host.js that the slice below reads, taken from host.js itself. + const phoneFrom=source.indexOf('const PHONE_TITLES ='),phoneTo=source.indexOf('\n',phoneFrom);assert(phoneFrom>0&&phoneTo>phoneFrom);vm.runInContext(source.slice(phoneFrom,phoneTo),ctx); + const watchFrom=source.indexOf('let wakeProbe = 0, plotHeard = 0;'),watchTo=source.indexOf('const onWake = why =>',watchFrom);assert(watchFrom>0&&watchTo>watchFrom);vm.runInContext(source.slice(watchFrom,watchTo),ctx); vm.runInContext(source.slice(a,b),ctx);ctx.showViewStatus=s=>status.push(s); - return{ctx,calls,status,requests,sockets,VIEW_LIMITS,boot,source,runBoot:async()=>{let started;const start=ctx.startThin;ctx.startThin=()=>started=start();vm.runInContext(boot,ctx);await Promise.resolve();await started;},tick:ms=>{clock+=ms;for(const[id,t]of[...timers])if(t.at<=clock&&timers.has(id)){timers.delete(id);t.fn();}}}; + return{ctx,calls,status,requests,sockets,VIEW_LIMITS,DIRECT_TRIES,boot,source,runBoot:async()=>{let started;const start=ctx.startThin;ctx.startThin=()=>started=start();vm.runInContext(boot,ctx);await Promise.resolve();await started;},tick:ms=>{clock+=ms;for(const[id,t]of[...timers])if(t.at<=clock&&timers.has(id)){timers.delete(id);t.fn();}}}; } test('actual startup begins capability discovery despite a permanently pending focused IDB lookup',async()=>{ const f=await fixture(false);await f.runBoot();assert(f.calls.includes('focused-read'));assert(f.calls.includes('/client/capabilities'));assert.equal(f.ctx.viewMode,'read-only'); @@ -114,6 +120,8 @@ test('late retry probe cannot replace a newer connection; previous host path rep const f=await fixture(true,legacy);await f.runBoot();f.sockets[0].open(); const original=f.ctx.window.fetch;let release; f.ctx.window.fetch=url=>url==='/hello'?new Promise(resolve=>{release=resolve;}):original(url); + // A page past its direct tries asks /hello first (directReconnect); that probe is what can come back late. + vm.runInContext('directFails='+f.DIRECT_TRIES,f.ctx); f.sockets[0].close();vm.runInContext('thinRetry.wake()',f.ctx);assert.equal(typeof release,'function'); await f.ctx.startThin(false,'retry');const newer=f.sockets[1];newer.open(); release({status:200});await new Promise(setImmediate); diff --git a/desktop/test/thin-search-ui.test.cjs b/desktop/test/thin-search-ui.test.cjs index 9fc930c332..bfb38fad99 100644 --- a/desktop/test/thin-search-ui.test.cjs +++ b/desktop/test/thin-search-ui.test.cjs @@ -1,5 +1,6 @@ 'use strict'; -const {test}=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'); +const {test,before}=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'); +let softFocus;before(async()=>{({softFocus}=await import('../../src/web/touch-focus.js'));}); const source=fs.readFileSync(path.resolve(__dirname,'../../src/web/host.js'),'utf8'); function trigger(mode,ready){ const a=source.indexOf('function dispatch('),b=source.indexOf(' if (viewMode !== "compatibility")',a),panels=[],status=[]; @@ -24,9 +25,12 @@ function panelFixture(){ } const panel=new Element('section'),calls=[],select=[];let closes=0; const ctx={resultPanel:()=>({panel,close:()=>{closes++;connected=false;}}), - document:{createElement:tag=>new Element(tag),createTextNode:text=>({tag:'#text',textContent:text}),getElementById:id=>id==='find-rows'?panel.children[2]:null}, - thin:{search:(...args)=>{calls.push(args);return true;}},dispatch:(...args)=>select.push(args),later:()=>{},setTimeout,clearTimeout,Date}; - const a=source.indexOf('let thinFind = null;'),b=source.indexOf('function showThinMessage(',a);vm.createContext(ctx);vm.runInContext(source.slice(a,b),ctx); + document:{documentElement:{classList:{contains:()=>false}},createElement:tag=>new Element(tag),createTextNode:text=>({tag:'#text',textContent:text}),getElementById:id=>id==='find-rows'?panel.children[2]:null}, + thin:{search:(...args)=>{calls.push(args);return true;}},dispatch:(...args)=>select.push(args),later:()=>{},softFocus,setTimeout,clearTimeout,Date}; + const a=source.indexOf('let thinFind = null;'),b=source.indexOf('function showThinMessage(',a);vm.createContext(ctx); + // A top-level name of host.js that the slice reads, taken from host.js itself. + const phoneFrom=source.indexOf('const PHONE_TITLES ='),phoneTo=source.indexOf('\n',phoneFrom);assert(phoneFrom>0&&phoneTo>phoneFrom);vm.runInContext(source.slice(phoneFrom,phoneTo),ctx); + vm.runInContext(source.slice(a,b),ctx); const parts=()=>({query:panel.children[0],note:panel.children[1],rows:panel.children[2]}); const key=(k,more={})=>parts().query.handlers.keydown({key:k,isComposing:false,preventDefault(){},...more}); return{ctx,panel,calls,select,parts,key,closes:()=>closes}; diff --git a/desktop/test/thin-view-hold.test.cjs b/desktop/test/thin-view-hold.test.cjs index 9257258c54..54896802e1 100644 --- a/desktop/test/thin-view-hold.test.cjs +++ b/desktop/test/thin-view-hold.test.cjs @@ -78,7 +78,7 @@ test('actual held/wait markers reach the dataset and unchanged renders do not re const data={},writes=[];const ctx={document:{documentElement:{dataset:new Proxy(data,{set:(o,k,v)=>{writes.push(k);o[k]=v;return true;}})}}, frontendNode:null,transitionNode:{},transitionFor:'thread:A',focusedNode:null,canonicalBoot:false,heldAt:5000,QUIET_MS:1200, performance:{now:()=>5000},currentSession:()=>({sel:'B'}),presentationTarget:s=>({kind:'thread',id:s.sel}),targetKey:t=>t.kind+':'+t.id}; - const a=source.indexOf('function htmlData('),b=source.indexOf('function render()',a),c=source.indexOf(' const heldFor =',b),d=source.indexOf(' retireAllowance(',c);assert(a>0&&b>a&&c>b&&d>c); + const a=source.indexOf('function htmlData('),b=source.indexOf('function render()',a),c=source.indexOf(' const heldFor =',b),d=source.indexOf(' trace("dom",',c);assert(a>0&&b>a&&c>b&&d>c); vm.createContext(ctx);vm.runInContext(source.slice(a,b),ctx);const mark=()=>vm.runInContext('{'+source.slice(c,d)+'}',ctx); mark();assert.deepEqual(data,{backplaneHeld:'true',backplaneWait:'false'});mark();assert.equal(writes.length,2); ctx.performance.now=()=>6200;mark();assert.equal(data.backplaneHeld,'quiet');