/* Real production readiness lifecycle/render helpers with controlled I/O edges. */ const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'),assert=require('node:assert/strict'); const source=fs.readFileSync(path.join(__dirname,'../hockey_data/static/agent-diagnostics.js'),'utf8'); const tick=async()=>{for(let i=0;i<35;i++)await Promise.resolve();}; const plain=value=>JSON.parse(JSON.stringify(value)); function definition(name){ const match=new RegExp('(^|\\n)([ \\t]*)(?:async )?function '+name+'\\(').exec(source);assert.ok(match,name); const start=match.index+match[1].length,end=source.indexOf('\n'+match[2]+'}',start);assert.ok(end>start,name); return source.slice(start,end+match[2].length+2); } function payload(device='devA',game='gameA'){ const labels={selection:'Выбранный контекст',assignment:'Назначение матча',agent:'Agent',vmix:'vMix',inventory:'Структура проекта',publication:'Mapping',targets:'Цели Mapping',delivery:'Обработка и ACK',pending:'Ожидающие команды'}; const details={agent:{heartbeat_age_seconds:1},inventory:{current_connection:true,scanned_at:'2026-10-21T12:00:00Z',input_count:10,field_count:50},publication:{version:3},targets:{target_count:20,matched_count:20,missing_count:0},delivery:{confirmed:20,full_profile:true,completed_at:'2026-10-21T12:00:00Z',errors_count:0},pending:{count:0,mapping_busy:false}}; return {ok:true,device_id:device,game_id:game,checked_at:'2026-10-21T12:00:00Z',status:'ready',checks:Object.entries(labels).map(([key,label])=>({key,label,status:'ready',message:'Подтверждено',details:details[key]||{}}))}; } function harness(){ const values=new Map([['hockey.accountId','accountA'],['hockey.selectedGameId','oldGame'],['hockey.vmix.selected_device','oldDevice']]); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,String(value))}; let game='gameA',device='devA',now=0,timerId=0;const timers=new Map(),requests=[]; const document={hidden:false};let renders=0; const setTimer=(fn,ms)=>{const id=++timerId;timers.set(id,{at:now+ms,fn});return id;},clearTimer=id=>timers.delete(id); const window={HockeyAgentRuntime:{currentDeviceId:()=>device},UIBuilderRuntime:{getData:()=>({hockey:{selected_game:game?{external_id:game}:null}})},setTimeout:setTimer,clearTimeout:clearTimer}; const context={window,document,runtimeStorage:storage,AbortController,URLSearchParams,Date,console,setTimeout:setTimer,clearTimeout:clearTimer, render:()=>renders++,fetch:(url,options)=>new Promise((resolve,reject)=>{ const row={url,options,ignoreAbort:false,resolve:body=>resolve({ok:true,status:200,json:async()=>body}),fail:(status,detail)=>resolve({ok:false,status,json:async()=>({detail})})};requests.push(row); options.signal?.addEventListener('abort',()=>{if(!row.ignoreAbort)reject(new DOMException('Aborted','AbortError'));},{once:true}); if(url==='/api/hockey/system/diagnostics')row.resolve({ok:true,database:{ok:true},devices:[]}); })}; vm.createContext(context); const preamble=source.slice(source.indexOf(' const state ='),source.indexOf(' async function api(')); const names=['api','activeDeviceId','readinessSelection','invalidateReadiness','syncReadinessSelection','normalizedReadiness','readinessDetails','renderReadiness','loadReadiness','load','stopRequests','schedule','selectionChanged','formatClock','formatReadinessTime']; vm.runInContext(preamble+'\n'+names.map(definition).join('\n'),context); const state=vm.runInContext('state',context); return {context,state,window,document,storage,timers,requests,renders:()=>renders, readiness:()=>requests.filter(row=>row.url.startsWith('/api/hockey/system/readiness?')), select:(nextGame,nextDevice)=>{game=nextGame;device=nextDevice;}, advance:async ms=>{now+=ms;for(const[id,timer]of [...timers])if(timer.at<=now){timers.delete(id);timer.fn();}await tick();}}; } const cases={}; cases.closed_bootstrap=async()=>{ const h=harness();await h.context.loadReadiness();assert.equal(h.requests.length,0); await h.context.load(true,{bootstrap:true});assert.equal(h.requests.length,1);assert.equal(h.readiness().length,0,'Bootstrap added a readiness request'); h.context.selectionChanged();h.context.schedule();await tick();assert.equal(h.requests.length,1);assert.equal(h.timers.size,0); h.state.open=true;await tick();const work=h.context.loadReadiness();h.readiness()[0].resolve(payload());await work; assert.equal(h.state.readinessPayload.status,'ready'); h.state.open=false;h.context.stopRequests();h.context.schedule();assert.equal(h.state.readinessPayload,null); h.context.selectionChanged();await tick();assert.equal(h.readiness().length,1,'Closed panel refreshed readiness'); }; cases.coalesce_timeout=async()=>{ const h=harness();h.state.open=true; const first=h.context.loadReadiness(),second=h.context.loadReadiness();assert.equal(h.readiness().length,1); h.readiness()[0].resolve(payload());await Promise.all([first,second]);assert.equal(h.state.readinessPayload.status,'ready'); const slow=h.context.loadReadiness();await h.advance(10001);await slow; assert.equal(h.state.readinessPayload,null,'Timeout left an old green payload'); assert.match(h.state.readinessError,/Время проверки истекло/);assert.equal(h.state.readinessLoading,false);assert.equal(h.timers.size,0); assert.ok(!h.context.renderReadiness().includes('hockey-readiness-status ready')); }; cases.aba_stale=async()=>{ const h=harness();h.state.open=true; const oldA=h.context.loadReadiness();h.readiness()[0].ignoreAbort=true; h.select('gameB','devB');h.context.selectionChanged();await tick();h.readiness()[1].ignoreAbort=true; h.select('gameA','devA');h.context.selectionChanged();await tick(); assert.equal(h.readiness().length,3);assert.equal(h.state.readinessPayload,null); h.readiness()[0].resolve(payload());await oldA;await tick(); assert.equal(h.state.readinessPayload,null,'Old A response survived A→B→A generation fence');assert.equal(h.state.readinessLoading,true); h.readiness()[1].resolve(payload('devB','gameB'));await tick();assert.equal(h.state.readinessPayload,null); h.readiness()[2].resolve(payload());await tick();assert.equal(h.state.readinessPayload.status,'ready'); h.select('gameA','');h.context.selectionChanged();assert.equal(h.context.activeDeviceId(),'','Explicit empty Agent fell back to stale storage'); assert.equal(h.state.readinessPayload,null);assert.equal(h.readiness().length,3);assert.match(h.context.renderReadiness(),/Agent: не выбран/); h.select('','devA');h.context.selectionChanged();assert.equal(h.context.readinessSelection().gameId,'','Explicit empty match fell back to storage'); assert.equal(h.readiness().length,3); }; cases.account_hide_close=async()=>{ const h=harness();h.state.open=true;const old=h.context.loadReadiness();h.readiness()[0].ignoreAbort=true; h.storage.setItem('hockey.accountId','accountB');h.context.selectionChanged();await tick(); assert.equal(h.state.readinessPayload,null);assert.equal(h.readiness().length,2); h.readiness()[0].resolve(payload());await old;assert.equal(h.state.readinessPayload,null,'Previous-account payload rendered'); h.document.hidden=true;h.context.stopRequests();h.context.schedule();await tick(); assert.equal(h.state.readinessPayload,null);assert.equal(h.state.readinessLoading,false);assert.equal(h.timers.size,0); const count=h.readiness().length;await h.context.loadReadiness();assert.equal(h.readiness().length,count,'Hidden panel requested readiness'); h.document.hidden=false;h.context.selectionChanged();await tick();assert.equal(h.readiness().length,count+1); h.state.open=false;h.context.stopRequests();h.context.schedule();await tick(); assert.equal(h.timers.size,0);assert.equal(h.state.readinessPayload,null); }; cases.errors_payload=async()=>{ const h=harness();h.state.open=true; let work=h.context.loadReadiness();h.readiness()[0].fail(404,'foreign account details');await work; assert.equal(h.state.readinessPayload,null);assert.match(h.state.readinessError,/недоступен для этого аккаунта/);assert.ok(!h.state.readinessError.includes('foreign account details')); work=h.context.loadReadiness();h.readiness()[1].fail(503,'server unavailable');await work;assert.match(h.state.readinessError,/server unavailable/); work=h.context.loadReadiness();h.readiness()[2].resolve(payload('anotherDevice'));await work; assert.equal(h.state.readinessPayload,null);assert.match(h.state.readinessError,/не подтверждает выбранный матч/); work=h.context.loadReadiness();const incomplete=payload();incomplete.checks.pop();h.readiness()[3].resolve(incomplete);await work; assert.equal(h.state.readinessPayload,null);assert.match(h.state.readinessError,/Не все проверки/); work=h.context.loadReadiness();const invalid=payload();invalid.ok='true';h.readiness()[4].resolve(invalid);await work;assert.equal(h.state.readinessPayload,null); for(const[key,field,value]of [['delivery','confirmed',true],['delivery','full_profile','true'],['inventory','current_connection','true'],['pending','mapping_busy','false']]){ const input=payload();input.checks.find(check=>check.key===key).details[field]=value; assert.equal(h.context.normalizedReadiness(input,h.context.readinessSelection()).status,'unknown','Truthy strings became green'); } }; cases.render_readonly=async()=>{ const h=harness();h.state.open=true;const work=h.context.loadReadiness();const input=payload(); input.checks[0].message='';input.checks[0].label=''; input.checks.find(check=>check.key==='delivery').status='error';input.status='ready';h.readiness()[0].resolve(input);await work; assert.equal(h.state.readinessPayload.status,'error','Client promoted failed checks to green'); const html=h.context.renderReadiness();assert.ok(html.includes('<img'));assert.ok(!html.includes('')); assert.match(html,/Актуальность данных внешних источников.*не подтверждается/);assert.match(html,/визуально.*Countdown/); assert.match(html,/21\.10\.2026.*15:00:00.*МСК/,'Date/time must identify old observations in journal timezone'); const delivery=input.checks.find(check=>check.key==='delivery');delivery.details.full_profile=false; assert.match(h.context.readinessDetails(delivery),/>Обработка и ACK:/);assert.ok(!h.context.readinessDetails(delivery).includes('Полная обработка')); assert.ok(!html.includes('hockey-readiness-status ready')); h.state.readinessLoading=true;const refreshing=h.context.renderReadiness(); assert.equal((refreshing.match(/data-readiness-check=/g)||[]).length,9,'Refreshing collapsed the readable previous snapshot'); assert.match(refreshing,/Показан предыдущий результат/);assert.match(refreshing,/is-refreshing/);assert.ok(!refreshing.includes('hockey-readiness-status ready')); assert.ok(h.requests.every(row=>row.url.startsWith('/api/hockey/system/readiness?')||row.url==='/api/hockey/system/diagnostics')); assert.ok(h.requests.every(row=>!row.options.method||row.options.method==='GET'),'Readiness sent a mutation'); }; (async()=>{const name=process.argv[2];assert.ok(cases[name],name);await cases[name]();console.log('PASS readiness frontend: '+name);})().catch(error=>{console.error(error);process.exitCode=1;});