Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions desktop/test/focused-cache.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
Expand Down
18 changes: 13 additions & 5 deletions desktop/test/host-thin-startup.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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,
Expand All @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 8 additions & 4 deletions desktop/test/thin-search-ui.test.cjs
Original file line number Diff line number Diff line change
@@ -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=[];
Expand All @@ -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};
Expand Down
2 changes: 1 addition & 1 deletion desktop/test/thin-view-hold.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading