Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
109 lines
8.4 KiB
JavaScript
109 lines
8.4 KiB
JavaScript
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,/не текущее положение/);
|
|
});
|