feat(planning): consolidate recorded-route localization and spatial scene

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.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-21 08:47:19 +03:00
parent be58d589e2
commit e515ab1b8c
189 changed files with 19074 additions and 758 deletions
@@ -7673,6 +7673,26 @@ test("spatial controls explain the bounded K1 calibration wait before point data
}
});
test("planning status extends only its current authoritative acquiring session", () => {
const state = runtimeState();
const spatialActivity = {sessionId: "data-session-001", label: "Привязка к эталону", detail: "Ожидание на месте.", busy: true};
const render = (current, activity = spatialActivity, controllerPatch = {}) => renderToStaticMarkup(createElement(K1SpatialControlsView, {
controller: {...acquisitionController(current), ...controllerPatch}, spatialActivity: activity,
}));
assert.match(render(state), /Привязка к эталону/);
assert.match(render(state), /Ожидание на месте/);
assert.doesNotMatch(render(state, {...spatialActivity, sessionId: "other-capture"}), /Привязка к эталону/);
for (const acquisitionState of ["awaiting_external_start", "starting", "awaiting_external_stop", "stopping", "finalizing"]) {
const current = structuredClone(state); current.acquisition.state = acquisitionState;
assert.doesNotMatch(render(current), /Привязка к эталону/);
}
const lost = structuredClone(state); lost.connection_supervisor = supervisor({control: true, data: false, dataPlaneState: "stalled"});
assert.doesNotMatch(render(lost), /Привязка к эталону/);
assert.doesNotMatch(render(state, spatialActivity, {physicalStopInFlight: true}), /Привязка к эталону/);
const cleanup = structuredClone(state); cleanup.acquisition.cleanup_pending = true;
assert.doesNotMatch(render(cleanup), /Привязка к эталону/);
});
test("contour health never promotes selection or replay metrics to live authority", () => {
const selectedOnly = {
phase: "starting",
@@ -804,7 +804,7 @@ test("E40 reports historical visible evaluation with bounded camera-LiDAR case r
});
test("the primary point-cloud viewer restores from fullscreen on Escape", async () => {
const workspacesSource = await readFile(workspacesUrl, "utf8");
const workspacesSource = await readFile(new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url), "utf8");
assert.match(workspacesSource, /if \(!pointCloudFocused\) return;/);
assert.match(workspacesSource, /event\.key !== "Escape"/);
@@ -0,0 +1,108 @@
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,/не текущее положение/);
});
@@ -1066,7 +1066,7 @@ test("recording preparation never presents phase heartbeats as fake percentages"
test("unified recorded AI view keeps the raw camera mounted but does not cover overlays", async () => {
const workspaceSource = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url),
"utf8",
);
@@ -27,7 +27,7 @@ async function read(relativePath) {
test("Observatory is the third independent Polygon workspace", () => {
assert.deepEqual(
productModel.workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"],
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene", "mission-planner"],
);
assert.deepEqual(
productModel.workspaceById("observatory"),
@@ -0,0 +1,15 @@
import assert from 'node:assert/strict';
import {test} from 'node:test';
import {readFileSync} from 'node:fs';
test('planning live scene reuses the canonical vertical height range inside the existing stage',()=>{
const source=readFileSync(new URL('../src/components/missions/PlanningLiveScene.tsx',import.meta.url),'utf8');
assert.match(source,/RangeControl orientation="vertical" limitSide="left" label="Срез"/);
assert.match(source,/className="session-overview__height"/);
assert.match(source,/ceiling_m:ceilingRef\.current/);
assert.match(source,/formatLimit=\{value=>value\.toFixed\(1\)\.replace\('\.',','\)\}/);
assert.match(source,/heightBounds=null/);
assert.match(source,/const CLIP_CEILING_M=80/);
assert.match(source,/max:CLIP_CEILING_M/);
assert.match(source,/setBounds\(next\);setCeiling\(null\);/);
});
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import {before,after,test} from 'node:test';
import {createServer} from 'vite';
let server,createReporter;
before(async()=>{
server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}});
({createPlanningPresentationReporter:createReporter}=await server.ssrLoadModule(
'/src/core/missions/planningPresentationTelemetry.ts'));
});
after(async()=>{await server?.close();});
const settle=()=>new Promise(resolve=>setImmediate(resolve));
test('presentation observations batch separately from the scene channel and retain the proxy boundary',async()=>{
let timer,posted;
const reporter=createReporter({url:'/observations',schedule:callback=>{timer=callback;return 1;},cancel:()=>{},
fetcher:async(url,init)=>{posted={url,init};return new Response(null,{status:204});},
});
reporter.record({presentation:'live',cloudAgeMs:200,fitAgeMs:100,requestMs:40,
cloudRevision:3,cloudSequence:4,displayEpoch:'5d8c5d13-dfde-4ab6-9da0-475ab1f22c12'},
{rerunAdmissionMs:2,firstAnimationFrameMs:12,secondAnimationFrameMs:28});
assert.ok(timer);timer();await settle();
assert.equal(posted.url,'/observations');
const body=JSON.parse(posted.init.body);
assert.equal(body.schema_version,'missioncore.planning-browser-presentation/v1');
assert.deepEqual(body.samples,[{cloud_revision:3,cloud_sequence:4,
display_epoch:'5d8c5d13-dfde-4ab6-9da0-475ab1f22c12',request_ms:40,
rerun_admission_ms:2,first_animation_frame_ms:12,second_animation_frame_ms:28,
frame_timeout:false,source_to_second_animation_frame_upper_bound_ms:270}]);
reporter.dispose();
});
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import { before, after, test } from 'node:test';
import { readFile } from 'node:fs/promises';
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/planningProjects.ts');
});
after(async () => { await server?.close(); });
const item = {key: 'live:a', id: 'a', name: 'same name', kind: 'live', state: 'completed', revision: 2};
test('only terminal runs or drafts admit catalog deletion', () => {
for (const state of ['completed', 'cancelled', 'error', 'interrupted']) assert.equal(api.planningProjectDeletable({...item, state}), true);
for (const state of ['running', 'waiting', 'preparing', 'new-unknown']) assert.equal(api.planningProjectDeletable({...item, state}), false);
assert.equal(api.planningProjectDeletable({...item, kind: 'draft', state: 'draft'}), true);
assert.equal(api.planningProjectDeletable({...item, kind: 'recorded', state: 'ready'}), true);
});
test('delete sends exact kind/id/revision and requires a matching receipt', async t => {
const calls = [];
let receipt = {key: item.key, deleted: true};
t.mock.method(globalThis, 'fetch', async (...args) => { calls.push(args); return {ok: true, json: async () => receipt}; });
await api.deletePlanningProject(item);
assert.equal(calls[0][0], '/api/v1/mission-planner/projects/live/a');
assert.equal(calls[0][1].method, 'DELETE');
assert.deepEqual(JSON.parse(calls[0][1].body), {revision: 2});
receipt = {key: 'live:another-same-name', deleted: true};
await assert.rejects(api.deletePlanningProject(item), /не подтверждено/);
const count = calls.length;
await assert.rejects(api.deletePlanningProject({...item, state: 'running'}), /завершите/);
assert.equal(calls.length, count);
});
test('server refusal stays an error, never a successful deletion', async t => {
t.mock.method(globalThis, 'fetch', async () => ({ok: false, json: async () => ({detail: 'Проект изменён.'})}));
await assert.rejects(api.deletePlanningProject(item), /Проект изменён/);
});
test('UI uses canonical row actions and modal, and stale loads cannot resurrect a deleted item', async () => {
const component = await readFile(new URL('../src/components/missions/PlanningProjectSelect.tsx', import.meta.url), 'utf8');
const hook = await readFile(new URL('../src/core/missions/usePlanningProjects.ts', import.meta.url), 'utf8');
assert.match(component, /<Select/); assert.match(component, /<ConfirmationModal/);
assert.match(component, /disabled: !planningProjectDeletable\(item\)/);
assert.match(component, /await onRemove\(target\); setTarget\(null\)/);
assert.match(component, /<ToastStack/);
assert.match(hook, /removedKeys.current.has\(item.key\)/);
assert.match(hook, /removedKeys.current.has\(key\)/);
assert.match(hook, /if \(selectedKey.current === project.key\) select\(''\)/);
});
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict';
import {before,after,test} from 'node:test';
import {createServer} from 'vite';
import {readFileSync} from 'node:fs';
let server,start;
before(async()=>{server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}});({startPlanningSceneStream:start}=await server.ssrLoadModule('/src/core/missions/planningSceneStream.ts'));});
after(async()=>{await server?.close();});
const settle=()=>new Promise(resolve=>setImmediate(resolve));
function fixture(){
let sequence=0,state={active:true,revision:1,options:{mode:'3d'}},clock=0;
const timers=new Map(),requests=[],applied=[],errors=[];
const stop=start({url:'/scene',snapshot:()=>state,now:()=>clock,
schedule:(fn,ms)=>{const id=++sequence;timers.set(id,{fn,ms});return id;},cancel:id=>timers.delete(id),
fetcher:(url,init)=>new Promise((resolve,reject)=>requests.push({url,init,resolve,reject})),
apply:bytes=>applied.push(bytes),error:()=>errors.push(true)});
return {timers,requests,applied,errors,stop,setState:value=>{state=value;},setClock:value=>{clock=value;},
next:()=>{const [id,timer]=[...timers].find(([,t])=>t.ms!==2500);timers.delete(id);timer.fn();},
answer:(index,cursor='next',status=200)=>requests[index].resolve(new Response(status===204?null:new Uint8Array([1,2]),{status,headers:{'X-Planning-Scene-Cursor':cursor}}))};
}
test('one pending fetch, native cadence, no-op response and cancellation',async()=>{
const f=fixture();assert.equal(f.requests.length,1);
assert.equal(f.timers.size,1); // deadline only: no overlapping polling interval
f.setClock(35);f.answer(0);await settle();
assert.equal(f.applied.length,1);assert.equal([...f.timers.values()][0].ms,65);
f.next();assert.match(f.requests[1].url,/cursor=next/);
f.answer(1,'next',204);await settle();assert.equal(f.applied.length,1);
f.next();f.stop();assert.equal(f.requests[2].init.signal.aborted,true);
f.answer(2);await settle();assert.equal(f.applied.length,1);assert.equal(f.timers.size,0);
});
test('a changed mode or ended run discards the in-flight response and rebases',async()=>{
for(const next of [{active:true,revision:2,options:{mode:'top'}},{active:false,revision:2,options:{mode:'3d'}}]){
const f=fixture();f.setState(next);f.answer(0);await settle();
assert.equal(f.applied.length,0);f.next();assert.match(f.requests[1].url,/base=true/);
f.answer(1);await settle();assert.equal(f.applied.length,1);f.stop();
}
});
test('failure hides stale evidence, repairs geometry but retains admitted camera cursor',async()=>{
const f=fixture();f.answer(0);await settle();f.next();
f.requests[1].reject(new Error('offline'));await settle();
assert.equal(f.errors.length,1);assert.equal([...f.timers.values()][0].ms,500);
f.next();assert.match(f.requests[2].url,/base=true/);assert.match(f.requests[2].url,/cursor=next/);
f.stop();f.answer(2);await settle();assert.equal(f.applied.length,1);
});
test('display changes and discarded responses retain camera identity; reset intent reaches server',async()=>{
const f=fixture();f.answer(0,'admitted-camera');await settle();f.next();
f.setState({active:true,revision:1,options:{mode:'3d',ceiling_m:3,reset:0}});
f.answer(1,'discarded-camera');await settle();
assert.equal(f.applied.length,1);f.next();
assert.match(f.requests[2].url,/cursor=admitted-camera/);
assert.match(f.requests[2].url,/base=true/);
assert.match(f.requests[2].url,/reset=0/);
f.answer(2,'clipped');await settle();
f.setState({active:true,revision:1,options:{mode:'3d',ceiling_m:3,reset:1}});
f.next();assert.match(f.requests[3].url,/reset=1/);
f.answer(3,'reset');await settle();f.stop();
});
test('unchanged terminal views stop transfer but notice a later revision',async()=>{
const f=fixture();f.setState({active:false,revision:2,options:{mode:'3d'}});
f.answer(0);await settle();f.next();f.answer(1);await settle();f.next();
assert.equal(f.requests.length,2);
f.setState({active:false,revision:3,options:{mode:'3d'}});f.next();
assert.equal(f.requests.length,3);f.stop();f.answer(2);await settle();
});
test('cloud or fit expiring during delivery cannot revive green',async()=>{
for(const [cloud,fit] of [['1.8','1'],['.1','7.8'],['invalid','1']]){
const f=fixture();f.setClock(300);
f.requests[0].resolve(new Response(new Uint8Array([1]),{headers:{
'X-Planning-Scene-Cursor':'next','X-Planning-Presentation':'live',
'X-Planning-Cloud-Age':cloud,'X-Planning-Fit-Age':fit}}));
await settle();assert.equal(f.applied.length,0);assert.equal(f.errors.length,1);f.stop();
}
});
test('a live response waits for bounded browser delivery and passes its exact receipt identity',async()=>{
let release,delivery;
let sequence=0,state={active:true,revision:1,options:{mode:'3d'}},clock=0;
const timers=new Map(),requests=[];
const stop=start({url:'/scene',snapshot:()=>state,now:()=>clock,
schedule:(fn,ms)=>{const id=++sequence;timers.set(id,{fn,ms});return id;},cancel:id=>timers.delete(id),
fetcher:(url,init)=>new Promise(resolve=>requests.push({url,init,resolve})),
apply:(_bytes,value)=>{delivery=value;return new Promise(resolve=>{release=resolve;});},error:assert.fail,
});
clock=40;
requests[0].resolve(new Response(new Uint8Array([1]),{headers:{
'X-Planning-Scene-Cursor':'next','X-Planning-Presentation':'live',
'X-Planning-Cloud-Age':'.2','X-Planning-Fit-Age':'.1',
'X-Planning-Cloud-Revision':'3','X-Planning-Cloud-Sequence':'4',
'X-Planning-Display-Epoch':'5d8c5d13-dfde-4ab6-9da0-475ab1f22c12',
'X-Planning-Height-Min':'-1.2','X-Planning-Height-Max':'51.5',
}}));
await settle();
assert.deepEqual(delivery,{presentation:'live',cloudAgeMs:200,fitAgeMs:100,requestMs:40,
cloudRevision:3,cloudSequence:4,displayEpoch:'5d8c5d13-dfde-4ab6-9da0-475ab1f22c12',
heightMinM:-1.2,heightMaxM:51.5});
assert.equal(timers.size,1);
assert.equal([...timers.values()][0].ms,2500);
release();await settle();
assert.equal([...timers.values()][0].ms,60);
stop();
});
test('profile scene can expand after native capture ends and retains Escape',()=>{
const source=readFileSync(new URL('../src/workspaces/spatial/SpatialWorkspace.tsx',import.meta.url),'utf8');
assert.match(source,/pointCloudVisible && \(visualProfile \|\| sourceUrl.trim\(\)\)/);
assert.match(source,/<IconButton\s+className="scene-source-control"\s+label="Развернуть облако точек"/);
assert.match(source,/event.key !== "Escape"/);
});
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import {before,after,test} from 'node:test';
import {createServer} from 'vite';
import {createElement} from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {readFileSync} from 'node:fs';
let server,Tool,Actions;
before(async()=>{
server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}});
({PlanningSceneToolWindow:Tool}=await server.ssrLoadModule('/src/components/missions/PlanningSceneToolWindow.tsx'));
({SpatialToolbarActions:Actions}=await server.ssrLoadModule(new URL('../../../packages/spatial-ui/src/SpatialToolbarActions.tsx',import.meta.url).pathname));
});
after(async()=>{await server?.close();});
test('scene tools use the bounded modeless window with move, resize and expand controls',()=>{
const html=renderToStaticMarkup(createElement(Tool,{boundsRef:{current:null},title:'Слои',onClose:()=>{}},'CONTROLS'));
assert.match(html,/nodedc-workspace-window/);
assert.match(html,/aria-modal="false"/);
assert.match(html,/Переместить инструмент/);
assert.match(html,/Изменить размер инструмента/);
assert.match(html,/Развернуть инструмент/);
assert.doesNotMatch(html,/nodedc-overlay/);
});
test('spatial toolbar omits source and duplicate planning navigation',()=>{
const html=renderToStaticMarkup(createElement(Actions,{openLayers:()=>{},openDisplay:()=>{},activeTool:'layers'}));
assert.match(html,/Слои/);assert.match(html,/Отображение/);
assert.match(html,/aria-pressed="true"/);assert.doesNotMatch(html,/Движок|Планирование/);
const workspace=readFileSync(new URL('../src/workspaces/missions/PlanningSpatialWorkspace.tsx',import.meta.url),'utf8');
assert.doesNotMatch(workspace,/<Window\s|openSource=|openView\('mission-planner'\)/);
assert.match(workspace,/tools:boundsRef=>tool&&<PlanningSceneToolWindow/);
const viewer=readFileSync(new URL('../src/components/missions/PlanningLiveScene.tsx',import.meta.url),'utf8');
assert.match(viewer,/\},\[runId,retry\]\)/); // Presentation never owns viewer lifetime.
});
@@ -324,7 +324,7 @@ test("Polygon exposes one dataset surface and keeps legacy links compatible", ()
assert.equal(workspaceById("datasets").kind, "datasets");
assert.deepEqual(
workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"],
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene", "mission-planner"],
);
assert.equal(
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
@@ -29,7 +29,7 @@ test("top navigation has no Center and Park owns contour health first", () => {
assert.equal(productModel.workspaceById("contour-health")?.root, "fleet");
assert.deepEqual(
productModel.workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"],
["lab-archive", "simulations", "observatory", "local-device", "spatial-scene", "mission-planner"],
);
assert.equal(productModel.workspaceById("spatial-scene")?.root, "polygon");
assert.equal(productModel.workspacesForRoot("observation").some(({ id }) => id === "spatial-scene"), false);
@@ -295,7 +295,7 @@ test("loading and error overlays fully conceal recorded camera pixels", async ()
test("point-cloud fullscreen keeps the admitted recorded camera worker mounted", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url),
"utf8",
);
assert.match(source, /\{visibleMediaSources\.map\(\(source, index\) => \(/);
@@ -320,7 +320,7 @@ test("one live document owns one native Rerun receiver", async () => {
test("raw replay exercises the same streaming receiver lifecycle as a live scan", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url),
"utf8",
);
assert.match(
@@ -343,7 +343,7 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
);
assert.match(
source,
/const recordedSource = Boolean\(recordedReplay\) \|\| \/\\\.rrd/,
/const recordedSource = !visualProfile && \(Boolean\(recordedReplay\) \|\| \/\\\.rrd/,
);
assert.doesNotMatch(
source,
@@ -353,7 +353,7 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
test("pending K1 STOP keeps the live Rerun source mounted until local capture ends", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url),
"utf8",
);
assert.match(
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import { before, after, test } from 'node:test';
import { createServer } from 'vite';
import { readFile } from 'node:fs/promises';
let server, api;
before(async () => {
server = await createServer({ appType: 'custom', logLevel: 'silent', server: { middlewareMode: true } });
api = await server.ssrLoadModule('/src/core/observation/sessionOverview.ts');
});
after(async () => { await server?.close(); });
test('height slice delegates endpoint labels and unit-bearing value to the shared range', async () => {
const source = await readFile(new URL('../src/components/observation/SessionOverviewScene.tsx', import.meta.url), 'utf8');
assert.match(source, /RangeControl orientation="vertical" limitSide="left" label="Срез"/);
assert.match(source, /formatLimit=\{value => value\.toFixed\(1\)\.replace\('\.', ','\)\}/);
assert.match(source, /formatValue=\{value => `\$\{value\.toFixed\(1\)\.replace\('\.', ','\)\} м`\}/);
assert.doesNotMatch(source, /<span>\{(?:high|low)\.toFixed\(1\)\} м<\/span>/);
});
test('interval chart retains the largest pause and rejects invalid values', () => {
const result = api.overviewChartPoints([[0, .1], [30, 1.2], [60, .1], [NaN, 5]], 100, 50);
assert.equal(result.xmax, 60);
assert.equal(result.ymax, 1.32);
assert.equal(result.points.split(' ').length, 3);
assert.doesNotMatch(result.points, /NaN/);
});
test('session overview rejects a result belonging to a different session', async () => {
const original = globalThis.fetch;
globalThis.fetch = async () => ({ ok: true, json: async () => ({ schema_version: 'missioncore.session-overview/v1', state: 'ready', session: { session_id: 'other' } }) });
try { await assert.rejects(api.fetchSessionOverview('selected', new AbortController().signal), /некорректные/); }
finally { globalThis.fetch = original; }
});
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { before, after, test } from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { createServer } from 'vite';
let server, SpatialScene;
before(async () => {
server = await createServer({ appType: 'custom', logLevel: 'silent', server: { middlewareMode: true } });
({ SpatialScene } = await server.ssrLoadModule(new URL('../../../packages/spatial-ui/src/SpatialScene.tsx', import.meta.url).pathname));
});
after(async () => { await server?.close(); });
const render = (focused = false) => renderToStaticMarkup(createElement(SpatialScene, {
viewportRef: { current: null }, primaryFocused: focused, toolbar: null, renderer: null,
sourceControls: createElement('div', { className: focused ? 'scene-focus-exit' : 'scene-source-controls' }, 'SOURCE_CONTROLS'),
status: { label: 'Накопление данных', tone: 'neutral', message: 'Сканер неподвижен.' },
metrics: createElement('div', null, 'METRICS'),
}));
test('scene tools, visual engine and metrics share one top-left flow in that order', () => {
const markup = render();
const stack = markup.indexOf('class="scene-information"');
const controls = markup.indexOf('SOURCE_CONTROLS');
const status = markup.indexOf('ВИЗУАЛЬНЫЙ ДВИЖОК');
const metrics = markup.indexOf('METRICS');
assert.ok(stack < controls && controls < status && status < metrics);
assert.equal((markup.match(/SOURCE_CONTROLS/g) ?? []).length, 1);
assert.doesNotMatch(markup, /scene-status--top-left/);
});
test('focus exit remains viewport-owned outside the hidden information stack', () => {
const markup = render(true);
assert.ok(markup.indexOf('scene-focus-exit') < markup.indexOf('scene-information'));
assert.match(markup, /class="scene-information" aria-hidden="true"/);
assert.equal((markup.match(/SOURCE_CONTROLS/g) ?? []).length, 1);
});
test('scene layout uses flow, retains compact metrics and removes only the calibration perimeter', async () => {
const css = await readFile(new URL('../../../packages/spatial-ui/src/spatial.css', import.meta.url), 'utf8');
const responsive = await readFile(new URL('../src/styles/responsive.css', import.meta.url), 'utf8');
const calibration = await readFile(new URL('../../../plugins/xgrids-k1/frontend/src/components/K1SpatialSession.css', import.meta.url), 'utf8');
assert.match(css, /\.scene-information \{[^}]*position: absolute;[^}]*display: grid;/);
assert.match(css, /\.scene-information > \.scene-source-controls \{ position: static; pointer-events: auto;/);
assert.match(css, /\.scene-information\[aria-hidden="true"\] \{ display: none;/);
assert.doesNotMatch(css.match(/\.scene-metrics \{[^}]*\}/)?.[0] ?? '', /top:|right:|position: absolute/);
assert.doesNotMatch(responsive, /\.scene-metrics\s*\{\s*display: none/);
assert.match(calibration, /\.xgrids-k1-spatial-controls \{[^}]*border: 0;/);
});