import assert from 'node:assert/strict'; import { before, after, test } from 'node:test'; import { createServer } from 'vite'; let server, api; before(async () => { server = await createServer({ appType: 'custom', logLevel: 'silent', server: { middlewareMode: true } }); api = await server.ssrLoadModule('/src/core/missions/planner.ts'); }); after(async () => { await server?.close(); }); const source = { schema_version:'missioncore.planning-source/v1', session_id:'A', generation:'a'.repeat(64), units:'m', poses:Array.from({length:5}, (_, i) => ({index:i, position:[i*3,i*4,0], distance_m:i*5})) }; test('route reverses source order without changing geometry or source', () => { const result = api.selectedPoses(source, 1, 3, 'reverse'); assert.deepEqual(result.map(p => p.index), [3,2,1]); assert.equal(api.routeLength(result), 10); assert.equal(source.poses[0].index,0); }); test('bounded section uses travelled distance, including out-and-back', () => { assert.equal(api.endAtDistance(source, 1, 7),3); assert.equal(api.endAtDistance(source, 1, 30),4); assert.deepEqual(api.selectedPoses(source,4,2,'forward'),[]); }); test('catalog excludes derived LAB parent and nonspatial sessions from zone binding', () => { const item={replayable:true,modalities:['point-cloud','trajectory']}; assert.equal(api.canSelectSession(item),true); assert.equal(api.canSelectSession({...item,lab:{}}),false); assert.equal(api.canSelectSession({...item,modalities:['video']}),false); }); test('source contract rejects a different session and nonfinite coordinates', () => { assert.equal(api.validatePlanningSource(source,'A'),source); assert.throws(() => api.validatePlanningSource(source,'B')); assert.throws(() => api.validatePlanningSource({...source,poses:[{index:0,position:[0,0,NaN],distance_m:0},source.poses[1]]},'A')); }); test('meter input selects the correct ordered pose and respects the selected branch', () => { assert.equal(api.indexAtDistance(source, 10), 2); assert.equal(api.indexAtDistance(source, 10, 3), 3); assert.equal(api.indexAtDistance(source, 100), 4); }); test('live distance has no upper cap and endpoint selection does not overshoot typed metres', () => { for (const n of [3, 30, 50, 100, 200, 300, 10000]) assert.equal(api.canStartPlanningRoute(n, 'scanner'), true); for (const n of [2.9, NaN, Infinity]) assert.equal(api.canStartPlanningRoute(n, 'scanner'), false); assert.equal(api.canStartPlanningRoute(50, 'recording'), false); assert.equal(api.indexAtDistance(source, 9.9, 1, 4, true), 1); assert.equal(api.indexAtDistance(source, 10, 1, 4, true), 2); assert.equal(api.indexAtDistance(source, 1, 1, 4, true), 1); }); test('planning match never promotes stopped or stale evidence', async()=>{ const {planningMatchCurrent}=await server.ssrLoadModule('/src/core/missions/planningPresentation.ts'); const t={state:'running',tracking_state:'tracking',stale:false,frame_age_s:0,result_age_s:2,result:{status:'candidate'}}; assert.equal(planningMatchCurrent(t),true); for(const patch of [{state:'completed'},{state:'waiting'},{tracking_state:'acquiring'},{tracking_state:'lost'},{tracking_state:undefined},{stale:true},{frame_age_s:8},{result_age_s:8},{result_age_s:null},{result:{status:'rejected'}}])assert.equal(planningMatchCurrent({...t,...patch}),false); assert.equal(planningMatchCurrent(t,true),false); }); test('planning failure is visible even when its last sample is stale', async()=>{ const {planningStatus}=await server.ssrLoadModule('/src/core/missions/planningPresentation.ts'); const t={state:'error',message:'Разрыв координат',stale:true,frame_age_s:null,result_age_s:null,result:null}; assert.deepEqual(planningStatus(t),{label:'Совмещение остановлено',tone:'danger',message:'Разрыв координат',pulse:true}); assert.equal(planningStatus({...t,state:'waiting'}).label,'Ожидание данных'); }); test('planning activity continues scanner preparation without presenting a prior as tracking', async()=>{ const {planningStatus,planningActivity}=await server.ssrLoadModule('/src/core/missions/planningPresentation.ts'); const t={state:'running',query_session_id:'B',tracking_state:'acquiring',stale:false,frame_age_s:0,result_age_s:null,result:null,message:'Ожидание на месте.'}; assert.equal(planningActivity({...t,planning_phase:'waiting-cloud'}),undefined); for(const [planning_phase,label] of [['collecting','Накопление данных'],['searching','Поиск положения на маршруте'],['refreshing','Подтверждение привязки'],['validating','Подтверждение привязки']]){ assert.deepEqual(planningActivity({...t,planning_phase}),{sessionId:'B',label,detail:'',busy:true}); assert.equal(planningStatus({...t,planning_phase}).tone,'neutral'); } const live={...t,planning_phase:'tracking',tracking_state:'tracking',result_age_s:0,result:{status:'candidate'},message:'Можно начинать проверочный проход.'}; assert.equal(planningActivity(live).busy,false); assert.equal(planningStatus(live).tone,'success'); const stale=planningStatus({...live,frame_age_s:9}); assert.equal(stale.label,'Привязка потеряна');assert.doesNotMatch(stale.message,/Можно начинать/); assert.equal(planningActivity({...t,planning_phase:'searching'},'Нет связи').busy,false); assert.equal(planningActivity({...live,query_session_id:null}),undefined); assert.equal(planningActivity({...live,state:'completed'}),undefined); const recovery=planningStatus({...t,planning_phase:'recovering',tracking_established:true}); assert.equal(recovery.pulse,true); assert.equal(recovery.tone,'danger'); assert.match(recovery.message,/запись продолжается/); }); test('project archive restores exact run identity and never promotes a failed live probe', async()=>{ const {defaultPlanningProject,planningProjectStatus}=await server.ssrLoadModule('/src/core/missions/planningProjects.ts'); const items=[{key:'live:failed',kind:'live',state:'error',result_status:null},{key:'recorded:second',kind:'recorded',state:'ready',result_status:'candidate'},{key:'recorded:first',kind:'recorded',state:'ready',result_status:'candidate'}]; assert.equal(defaultPlanningProject(items,null),'recorded:second'); assert.equal(defaultPlanningProject(items,'recorded:first'),'recorded:first'); assert.equal(defaultPlanningProject(items,'missing'),'recorded:second'); assert.equal(planningProjectStatus(items[0]),'Без результата совмещения'); assert.equal(defaultPlanningProject([],null),''); }); test('failed initial binding never claims previously established tracking was lost', async()=>{ const {planningStatus,planningActivity}=await server.ssrLoadModule('/src/core/missions/planningPresentation.ts'); const t={state:'running',query_session_id:'B',planning_phase:'lost',tracking_state:'lost',tracking_established:false, stale:true,frame_age_s:null,result_age_s:null,result:null,message:'Начальный поиск не завершён.'}; const failed=planningStatus(t); assert.equal(failed.label,'Маршрут не синхронизирован'); assert.equal(failed.message,'Начальный поиск не завершён.'); assert.deepEqual(planningActivity(t),{sessionId:'B',label:failed.label,detail:failed.message,busy:false}); assert.equal(planningStatus({...t,tracking_established:true}).label,'Привязка потеряна'); assert.equal(planningStatus({...t,state:'completed'}).label,'Исследование завершено'); }); test('freshness of displayed alignment fences green independently of the fit age',async()=>{ const {planningMatchCurrent,planningStatus}=await server.ssrLoadModule('/src/core/missions/planningPresentation.ts'); const t={state:'running',tracking_state:'tracking',planning_phase:'tracking',tracking_established:true, stale:false,frame_age_s:.2,result_age_s:4,result:{status:'candidate'},message:'Привязка подтверждена.',presentation_state:'live'}; assert.equal(planningMatchCurrent(t),true); assert.equal(planningMatchCurrent({...t,presentation_state:'historical'}),false); assert.equal(planningMatchCurrent({...t,frame_age_s:2.1}),false); assert.match(planningStatus({...t,presentation_state:'historical'}).message,/последней принятой/); assert.match(planningStatus({...t,state:'completed',presentation_state:'historical'}).message,/не текущее положение/); });