feat: finalize corrected-route planning and Rerun recording review

This commit is contained in:
DCCONSTRUCTIONS
2026-09-22 10:10:03 +03:00
parent c804d89b18
commit 2e5d52521f
132 changed files with 14141 additions and 898 deletions
@@ -184,6 +184,15 @@ function preparation(overrides = {}) {
};
}
test("replay preserves an optional corrected-map identity and rejects malformed versions", () => {
const decoded = decodeObservationSessionReplay(replay({ map_generation: "b".repeat(64) }));
assert.equal(decoded.mapGeneration, "b".repeat(64));
assert.equal(decodeObservationSessionReplay(replay()).mapGeneration, undefined);
for (const value of [null, "latest", "../private", 123]) {
assert.throws(() => decodeObservationSessionReplay(replay({ map_generation: value })), /версию/);
}
});
const preparationEtag = '"prepare-20260717T131400Z"';
test("session catalog decodes canonical snake_case into a path-free camelCase model", () => {
@@ -412,7 +421,7 @@ test("data recordings keep the compact session dropdown and laboratory results s
assert.doesNotMatch(laboratorySource, /fetchAdvancedLaboratoryResults/);
});
test("recording preparation statuses share the viewer's left alignment", async () => {
test("recording preparation statuses occupy the viewer's upper-right corner", async () => {
const spatialStyles = await readFile(
new URL("../../../packages/spatial-ui/src/spatial.css", import.meta.url),
"utf8",
@@ -422,9 +431,10 @@ test("recording preparation statuses share the viewer's left alignment", async (
spatialStyles.indexOf(".scene-operation-status {"),
);
assert.match(statusStack, /left:\s*0\.85rem/);
assert.match(statusStack, /justify-items:\s*start/);
assert.doesNotMatch(statusStack, /right:/);
assert.match(statusStack, /right:\s*0\.85rem/);
assert.match(statusStack, /top:\s*0\.85rem/);
assert.match(statusStack, /justify-items:\s*end/);
assert.doesNotMatch(statusStack, /left:|bottom:/);
});
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
@@ -150,14 +150,15 @@ test("follow toggles retain the current recorded camera journal", () => {
assert.notEqual(afterReset, before);
});
test("observation camera windows tile from the bottom-right above the live timeline", () => {
test("observation camera windows tile from the bottom-left above the live timeline", () => {
const bounds = { width: 1280, height: 720 };
const left = initialObservationWindowRect(0, 2, bounds);
const right = initialObservationWindowRect(1, 2, bounds);
assert.equal(left.y, right.y);
assert.ok(left.x + left.width < right.x);
assert.equal(right.x + right.width, bounds.width - 18);
assert.equal(left.x, 18);
assert.ok(right.x + right.width <= bounds.width - 18);
assert.ok(right.y + right.height <= bounds.height - 64);
});
@@ -323,10 +324,14 @@ test("recorded observation timeline rejects empty and non-finite ranges", () =>
test("accumulation control normalizes UI values and distinguishes a single frame", () => {
assert.equal(normalizeAccumulationSeconds(-3), 0);
assert.equal(normalizeAccumulationSeconds(12.6), 13);
assert.equal(normalizeAccumulationSeconds(999), 120);
assert.equal(normalizeAccumulationSeconds(999), 999);
assert.equal(normalizeAccumulationSeconds(1800), 1800);
assert.equal(normalizeAccumulationSeconds(9999), 9999);
assert.equal(normalizeAccumulationSeconds(Number.NaN), 0);
assert.equal(formatAccumulationDuration(0), "Кадр");
assert.equal(formatAccumulationDuration(12), "12 с");
assert.equal(formatAccumulationDuration(1800), "30 мин");
assert.equal(formatAccumulationDuration(125), "2 мин 5 с");
});
test("spatial timeline renders synchronized accumulation and playback controls", () => {
@@ -606,6 +611,32 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
);
});
test("recorded tracking quantizes the native fractional-nanosecond cursor without accepting invalid times", async () => {
const bodies = [];
const request = currentTimeNs => fetchRecordedBlueprintRrd(
"/api/v1/observation-sessions/session-1/blueprint.rrd",
{accumulationSeconds: 10, showGrid: true, showPoints: true,
showTrajectory: true, pointSize: 0.5, colorMode: "intensity",
palette: "turbo", customColor: "#ffffff"},
{applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001"},
{origin: "http://127.0.0.1:8000", blueprintSessionId: "a".repeat(32),
cameraEye: {position: [3, 4, 5], lookTarget: [1, 2, 0], eyeUp: [0, 0, 1]},
eyeRelativeToTracking: true, currentTimeNs,
fetcher: async (_input, init) => {
bodies.push(JSON.parse(init.body));
return new Response(new Uint8Array([0x52, 0x52, 0x46, 0x32]), {
headers: {"Content-Type": "application/vnd.rerun.rrd"},
});
}},
);
await request(536_460_021_972.65625);
assert.equal(bodies[0].current_time_ns, 536_460_021_973);
for (const invalid of [-0.1, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
await assert.rejects(request(invalid), /Unsafe recorded blueprint request/);
}
assert.equal(bodies.length, 1);
});
test("recorded point colors use one strict same-origin component overlay", async () => {
const endpoint = resolveRecordedPointColorsUrl(
"/api/v1/observation-sessions/session-1/recording.rrd",
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict';
import {before,after,test} from 'node:test';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {createServer} from 'vite';
import {readFile} from 'node:fs/promises';
let server,usePlanner,Settings;
before(async()=>{
server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}});
({useMissionPlanner:usePlanner}=await server.ssrLoadModule('/src/core/missions/useMissionPlanner.ts'));
({PlanningProjectSettings:Settings}=await server.ssrLoadModule('/src/components/missions/PlanningProjectSettings.tsx'));
});
after(async()=>{await server?.close();});
// Bounded hook dispatcher: execute the real state/effect transitions, without
// mounting another browser or replacing the production hook with a fake.
function harness(){
const slots=[];let index=0,pending=[];
const hooks={
useState(initial){const i=index++;if(!slots[i])slots[i]={value:initial};return [slots[i].value,v=>{slots[i].value=typeof v==='function'?v(slots[i].value):v;}];},
useEffect(fn,deps){const i=index++,old=slots[i];if(!old||deps.some((d,n)=>!Object.is(d,old.deps[n])))pending.push(()=>{old?.cleanup?.();slots[i]={deps,cleanup:fn()};});},
useMemo(fn){return fn();},useCallback(fn){return fn;},
};
const render=()=>{index=0;const internal=React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,old=internal.H;internal.H=hooks;try{return usePlanner();}finally{internal.H=old;}};
return {render,async settle(){let state;for(let n=0;n<5;n++){state=render();const effects=pending;pending=[];effects.forEach(fn=>fn());await new Promise(resolve=>setImmediate(resolve));}return render();},close(){slots.forEach(s=>s.cleanup?.());}};
}
const source={schema_version:'missioncore.planning-source/v1',session_id:'ring',generation:'a'.repeat(64),label:'Ring',units:'m',path_m:580,
poses:Array.from({length:60},(_,index)=>({index,position:[index*10,0,0],distance_m:index*10}))};
test('same recording reselection cannot collapse the reference or disable a named project',async()=>{
const oldFetch=globalThis.fetch,requests=[];const h=harness();
globalThis.fetch=async(url,options={})=>{
requests.push({url,body:options.body&&JSON.parse(options.body)});
if(options.method==='POST')return {ok:true,json:async()=>({id:'draft',revision:1,name:'Seam',zone:{session_id:'ring',generation:source.generation},route:{start_index:0,end_index:59,direction:'forward'}})};
return {ok:true,json:async()=>url.includes('/sources/')?source:{items:[],next_cursor:null}};
};
try{
let p=await h.settle();p.chooseSource('ring');p.setName('Seam');p=await h.settle();
assert.equal(p.poses.length,60);assert.equal(p.ready,true);
for(let i=0;i<4;i++){p.chooseSource('ring');p=await h.settle();assert.equal(p.poses.length,60);assert.equal(p.ready,true);}
await p.save();const body=requests.find(r=>r.body)?.body;
assert.equal(body.whole_recording,true);assert.equal(body.start_index,undefined);assert.equal(body.end_index,undefined);
assert.ok(requests.some(r=>r.url.includes('scope=standalone')));
p=h.render();p.newDraft();p=await h.settle();p.chooseSource('ring');p.setName('Next');p=await h.settle();
assert.equal(p.poses.length,60);assert.equal(p.ready,true);
assert.equal(p.setStart,undefined);assert.equal(p.setEnd,undefined);
}finally{h.close();globalThis.fetch=oldFetch;}
});
test('product removes reference crop controls and Data requests independent captures',async()=>{
const settings=await readFile(new URL('../src/components/missions/PlanningProjectSettings.tsx',import.meta.url),'utf8');
assert.doesNotMatch(settings,/Участок эталона|Начало участка|Конец участка|30 м от начала участка|p\.set(Start|End)/);
assert.match(settings,/Вся запись/);
const selector=await readFile(new URL('../src/components/ObservationSessionSelect.tsx',import.meta.url),'utf8');
assert.match(selector,/scope: "standalone"/);
});
test('planner offers one direct live start without a repeated-pass mode or catalogue',async()=>{
const p={name:'Ring',poses:source.poses,source,sessionId:'ring',options:[{value:'ring',label:'Ring'}],direction:'forward',ready:true};
const render=(overrides={},starting=false)=>renderToStaticMarkup(React.createElement(Settings,{p:{...p,...overrides},starting,onStart:()=>{}}));
const ready=render();
assert.doesNotMatch(ready,/Повторный проход|Из записи|Источник повторного прохода|Повторная запись|После запуска/);
const startButton=html=>[...html.matchAll(/<button\b[^>]*>[\s\S]*?<\/button>/g)].map(m=>m[0]).find(button=>button.includes('Начать новый проход'));
assert.ok(startButton(ready));
assert.doesNotMatch(startButton(ready),/disabled=/);
for(const state of [{ready:false},{busy:true},{poses:source.poses.slice(0,1)}])assert.match(startButton(render(state)),/disabled=/);
assert.match(startButton(render({},true)),/disabled=/);
let started=0;
const tree=Settings({p,starting:false,onStart:()=>{started++;}});
tree.props.children[1].props.children[1].props.onClick();
assert.equal(started,1);
const workspace=await readFile(new URL('../src/workspaces/missions/MissionPlannerWorkspace.tsx',import.meta.url),'utf8');
assert.doesNotMatch(workspace,/useRegistrationTest|setMode|t\.run\(/);
assert.match(workspace,/live\.begin\(draft\)/);
});
@@ -1,101 +1,42 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server, createRecordedRerunCameraJournal, initialEye;
let server, readNativeRerunCameraEye;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent",
server: { middlewareMode: true } });
const module = await server.ssrLoadModule(
"/src/components/rerun/recordedRerunCameraJournal.ts");
createRecordedRerunCameraJournal = module.createRecordedRerunCameraJournal;
initialEye = module.RECORDED_RERUN_ORBITAL_EYE;
({ readNativeRerunCameraEye } = await server.ssrLoadModule(
"/src/components/rerun/recordedRerunCameraJournal.ts"));
});
after(async () => { await server?.close(); });
class FakeEvent {
constructor(type, init = {}) { this.type = type; Object.assign(this, init); }
}
class FakeTarget {
listeners = new Map();
emitted = [];
addEventListener(type, listener) {
const listeners = this.listeners.get(type) ?? [];
listeners.push(listener); this.listeners.set(type, listeners);
}
removeEventListener(type, listener) {
this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener));
}
dispatchEvent(event) {
this.emitted.push(event);
for (const listener of this.listeners.get(event.type) ?? []) listener(event);
return true;
test("camera snapshot copies exact native pose, without approximating input", () => {
const native = { position: [287, -74, 8], lookTarget: [286, -72, 0], eyeUp: [0, 0, 1] };
const eye = readNativeRerunCameraEye(native);
assert.deepEqual(eye, native);
native.position[0] = 999;
assert.equal(eye.position[0], 287);
assert.equal(readNativeRerunCameraEye(null), null);
});
test("invalid native pose fails closed instead of substituting a guessed camera", () => {
for (const value of [{}, "pose", { position: [NaN, 0, 1], lookTarget: [0, 0, 0], eyeUp: [0, 0, 1] }]) {
assert.throws(() => readNativeRerunCameraEye(value), /Invalid native Rerun camera/);
}
}
const radius = (eye) => Math.hypot(
eye.position[0] - eye.lookTarget[0],
eye.position[1] - eye.lookTarget[1],
eye.position[2] - eye.lookTarget[2],
);
test("recorded orbital eye tracks only 3D viewport navigation", () => {
const scope = new FakeTarget();
const timers = [];
Object.assign(scope, {
WheelEvent: FakeEvent,
requestAnimationFrame(callback) { callback(); return 1; },
setTimeout(callback) { timers.push(callback); return timers.length; },
});
const canvas = new FakeTarget();
Object.assign(canvas, {
clientHeight: 200,
isConnected: true,
getBoundingClientRect: () => ({
left: 100, right: 500, top: 50, bottom: 250, width: 400, height: 200,
}),
});
const journal = createRecordedRerunCameraJournal(canvas, scope);
journal.configure(initialEye, 0.46);
const pointer = (type, x, y, buttons) => new FakeEvent(type, {
clientX: x, clientY: y, button: 0, buttons, pointerId: 7, pointerType: "mouse",
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
});
canvas.dispatchEvent(pointer("pointerdown", 200, 100, 1));
scope.dispatchEvent(pointer("pointermove", 240, 120, 1));
scope.dispatchEvent(pointer("pointerup", 240, 120, 0));
assert.deepEqual(journal.current(), initialEye);
canvas.dispatchEvent(pointer("pointerdown", 380, 100, 1));
scope.dispatchEvent(pointer("pointermove", 420, 120, 1));
scope.dispatchEvent(pointer("pointerup", 420, 120, 0));
const rotated = journal.current();
assert.notDeepEqual(rotated.position, initialEye.position);
assert.ok(Math.abs(radius(rotated) - radius(initialEye)) < 1e-9);
journal.setMaxOrbitalRadius(40);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 380, clientY: 130, deltaX: 0, deltaY: 2_000, deltaMode: 0,
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
}));
assert.ok(Math.abs(radius(journal.current()) - 40) < 1e-9);
journal.configure(rotated, 0.46);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 0, clientY: 0, deltaX: 0, deltaY: -20, deltaMode: 0,
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
}));
const zoomed = journal.current();
assert.ok(Math.abs(radius(zoomed) - radius(rotated) * Math.exp(-20 / 200)) < 1e-9);
const snapshot = journal.current();
journal.setSpatialViewportStart(0.8);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 380, clientY: 130, deltaX: 0, deltaY: -20, deltaMode: 0,
}));
assert.deepEqual(journal.current(), snapshot);
journal.dispose();
});
test("iframe owner reads the native camera and installs no shadow input listeners", () => {
const owner = readFileSync(new URL("../src/components/rerun/recordedRerunOwner.ts", import.meta.url), "utf8");
const camera = readFileSync(new URL("../src/components/rerun/recordedRerunCameraJournal.ts", import.meta.url), "utf8");
assert.match(owner, /get_camera_eye\?\.\(\)/);
assert.doesNotMatch(owner + camera, /createRecordedRerunCameraJournal|addEventListener|Math\.exp/);
});
test("display blueprint activation carries the current native eye, not the embedded preset", () => {
const viewport = readFileSync(new URL("../src/components/RerunViewport.tsx", import.meta.url), "utf8");
assert.match(viewport, /!enablingFollow \? active\.getCameraEye\?\.\(\) \?\? undefined : undefined/);
assert.match(viewport, /requestBlueprint\(\s*firstEye,\s*firstEyeIsTrackingRelative,/);
assert.match(viewport, /currentTimeNs: eyeRelativeToTracking \? currentTimeNs : undefined/);
});
@@ -59,6 +59,17 @@ function bridge(native, mount = {}) {
return parent;
}
test("camera snapshot crosses the disposable realm as copied primitive data", () => {
const eye = { position: [300, 7, 8], lookTarget: [298, 9, 0], eyeUp: [0, 0, 1] };
const { facade, dispose } = bridge({ stop() {}, get_camera_eye: () => eye });
const received = facade.get_camera_eye();
assert.deepEqual(received, eye);
assert.notEqual(received, eye);
assert.notEqual(received.position, eye.position);
dispose();
assert.throws(() => facade.get_camera_eye(), /disposed/);
});
test("recorded realm terminates on close even if upstream stop throws", async (t) => {
const f = fixture(t, { stopFails: true });
const scope = createIsolatedRerunHost(f.host);
@@ -251,6 +251,10 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
assert.match(source, /recordedPerceptionLayers\.costmap,/);
assert.match(source, /recordedPerceptionLayers\.costmap !== undefined && status !== "ready"/);
assert.match(source, /perceptionLayers\.costmap === undefined \? \{\} : \{\s*show_costmap: perceptionLayers\.costmap,\s*reactivate_updates: true/s);
// Ordinary archives need activation too, not only the LAB costmap path.
assert.match(source, /requestBlueprint\(\s*firstEye,\s*firstEyeIsTrackingRelative,[\s\S]*?\n\s*true,\s*\);/);
// Display changes must not trigger a full-file camera-bounds scan.
assert.match(source, /currentTimeNs: eyeRelativeToTracking \? currentTimeNs : undefined/);
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
assert.doesNotMatch(source, /rerunViewerOpenOptions/);
assert.match(
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
@@ -7,13 +8,41 @@ const root = resolve(import.meta.dirname, "..");
const packageRoot = resolve(root, "node_modules/@rerun-io/web-viewer");
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
test("Mission Core uses the exact upstream Rerun 0.36.3 web package", () => {
test("Mission Core pins Rerun 0.36.3 with the bounded native navigation installer", () => {
const application = readJson(resolve(root, "package.json"));
const installed = readJson(resolve(packageRoot, "package.json"));
assert.equal(application.dependencies["@rerun-io/web-viewer"], "0.36.3");
assert.equal(installed.version, "0.36.3");
assert.equal(application.scripts.postinstall, undefined);
assert.equal(application.scripts.postinstall, "node scripts/install-rerun-navigation.mjs");
assert.equal(application.scripts.prebuild, application.scripts.postinstall);
});
test("native navigation artifacts and source patch match their provenance", () => {
const vendorRoot = resolve(root, "vendor/rerun-web-viewer-0.36.3");
const manifest = readJson(resolve(vendorRoot, "navigation-build.json"));
const sha = bytes => createHash("sha256").update(bytes).digest("hex");
assert.equal(manifest.upstreamVersion, "0.36.3");
assert.equal(manifest.upstreamCommit, "6ded109d33c549e98185f7c95fa8009d44e4adef");
assert.equal(sha(readFileSync(resolve(vendorRoot, "NODEDC_NAVIGATION.patch"))), manifest.patchSha256);
for (const [name, identity] of Object.entries(manifest.files)) {
if (!identity.artifact) continue;
assert.equal(sha(readFileSync(resolve(vendorRoot, identity.artifact))), identity.sha256, name);
assert.equal(sha(readFileSync(resolve(packageRoot, name))), identity.sha256, name);
}
});
test("generated JS and native WASM share one ABI including the camera snapshot", () => {
const glue = readFileSync(resolve(packageRoot, "re_viewer.js"), "utf8");
const wasm = new WebAssembly.Module(readFileSync(resolve(packageRoot, "re_viewer_bg.wasm")));
const names = new Set(WebAssembly.Module.exports(wasm).map(item => item.name));
assert.ok(names.has("webhandle_nodedc_camera_eye"));
for (const [, name] of glue.matchAll(/\bwasm\.([a-zA-Z_$][\w$]*)/g)) {
assert.ok(names.has(name), `Missing WASM export: ${name}`);
}
assert.match(readFileSync(resolve(packageRoot, "index.js"), "utf8"), /this\.#handle\.nodedc_camera_eye\(\)/);
assert.match(readFileSync(resolve(packageRoot, "index.d.ts"), "utf8"), /get_camera_eye\(\)/);
assert.match(readFileSync(resolve(packageRoot, "re_viewer.d.ts"), "utf8"), /nodedc_camera_eye\(\)/);
});
test("the active application never imports or installs the archived vendor fork", () => {
@@ -0,0 +1,134 @@
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 {readFile} from 'node:fs/promises';
import {runInNewContext} from 'node:vm';
import ts from 'typescript';
let server, profile, points, defaults, Controls, Timeline;
before(async () => {
server = await createServer({appType: 'custom', logLevel: 'silent', server: {middlewareMode: true}});
profile = await server.ssrLoadModule('/src/core/observation/sessionDisplayProfile.ts');
points = await server.ssrLoadModule('/src/core/observation/recordedPointDisplay.ts');
defaults = (await server.ssrLoadModule('/src/sceneSettings.ts')).defaultSceneSettings;
({SceneDisplayControls: Controls} = await server.ssrLoadModule('../../packages/spatial-ui/src/SceneDisplayControls.tsx'));
({ObservationTimeline: Timeline} = await server.ssrLoadModule('/src/components/ObservationTimeline.tsx'));
});
after(async () => {await server?.close();});
const document = (settings, id='session-a') => ({schema_version: 'missioncore.session-display-profile/v1',
session_id: id, scene_settings: profile.encodeSessionDisplay(settings)});
test('session profile round trips all display settings and fractional decimation', () => {
const settings = {...defaults, accumulationMaxSeconds: 600, accumulationSeconds: 590, pointDecimationPercent: 49.5};
assert.deepEqual(profile.decodeSessionDisplay(document(settings), 'session-a'), settings);
assert.throws(() => profile.decodeSessionDisplay(document(settings), 'session-b'), /другой записи/);
for (const percent of [-1, 100.1, NaN, Infinity]) {
assert.throws(() => profile.decodeSessionDisplay(document({...settings, pointDecimationPercent: percent}), 'session-a'));
}
for (const percent of [0, 50, 100]) assert.equal(profile.decodeSessionDisplay(document({...settings,
pointDecimationPercent: percent}), 'session-a').pointDecimationPercent, percent);
assert.equal(defaults.accumulationMaxSeconds, 180);
});
test('timeline uses per-session range instead of a fixed duration cap', () => {
const html = renderToStaticMarkup(createElement(Timeline, {accumulationSeconds: 47,
accumulationMaxSeconds: 600, onAccumulationChange: () => {}}));
assert.match(html, /max="600"/);
});
test('display exposes precise thinning only for saved playback', () => {
const props = {displayDraft: defaults, stageDisplayPatch: () => {}, commitDisplayPatch: () => {}, flushDisplaySettings: () => {}};
assert.match(renderToStaticMarkup(createElement(Controls, {...props, replayPresented: true})), /Прореживание облака/);
assert.doesNotMatch(renderToStaticMarkup(createElement(Controls, props)), /Прореживание облака/);
});
test('open Display previews 100%, 0% and accumulation; close alone persists the latest draft', async () => {
// Execute the actual composition callbacks with deterministic timers. This
// catches the former open-inspector early return, not just helper behavior.
const source = await readFile(new URL('../src/App.tsx', import.meta.url), 'utf8');
const ast = ts.createSourceFile('App.tsx', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const callbacks = new Map();
const names = ['stageDisplayPatch', 'flushDisplaySettings', 'commitDisplaySettings'];
let close;
function visit(node) {
if (ts.isVariableDeclaration(node) && names.includes(node.name.getText(ast))) {
callbacks.set(node.name.getText(ast), node.initializer.getText(ast));
}
if (ts.isBinaryExpression(node) && node.left.getText(ast) === 'closeDisplayRef.current') close = node.getText(ast);
ts.forEachChild(node, visit);
}
visit(ast);
assert.equal(callbacks.size, names.length); assert.ok(close);
const applied = [], saved = [], timers = new Map(); let timerId = 0;
const context = {
useCallback: fn => fn, displayWindowOpenRef: {current: true}, replayActiveRef: {current: true},
displayDraftRef: {current: {...defaults, pointDecimationPercent: 86}}, setDisplayDraft() {},
viewerSettingsCommitTimerRef: {current: null}, viewerSettingsQuietPeriodMs: 750,
sceneSettingsCommitterRef: {current: {enqueue: value => applied.push(value)}}, closeDisplayRef: {current: null},
sessionDisplayProfile: {edited() {}, save: value => saved.push(value)},
window: {setTimeout: fn => {timers.set(++timerId, fn); return timerId;}, clearTimeout: id => timers.delete(id)},
};
const code = [...callbacks].map(([name, init]) => `const ${name} = ${init};`).join('\n') +
`\n${close}; ({stageDisplayPatch, flushDisplaySettings, close: closeDisplayRef.current});`;
const actions = runInNewContext(ts.transpileModule(code, {compilerOptions: {target: ts.ScriptTarget.ES2022}}).outputText, context);
actions.stageDisplayPatch({pointDecimationPercent: 100});
assert.equal(applied.at(-1).pointDecimationPercent, 100);
actions.stageDisplayPatch({pointDecimationPercent: 0});
assert.equal(applied.at(-1).pointDecimationPercent, 0);
actions.stageDisplayPatch({accumulationSeconds: 180});
actions.flushDisplaySettings();
assert.equal(applied.at(-1).accumulationSeconds, 180);
actions.stageDisplayPatch({accumulationSeconds: 10});
actions.flushDisplaySettings();
assert.equal(applied.at(-1).accumulationSeconds, 10);
actions.stageDisplayPatch({pointDecimationPercent: 49.5});
assert.equal(timers.size, 1);
const pending = [...timers.values()][0]; timers.clear(); pending();
assert.equal(applied.at(-1).pointDecimationPercent, 49.5);
assert.equal(applied.at(-1).accumulationSeconds, 10);
assert.equal(saved.length, 0);
actions.close();
assert.equal(saved.length, 1);
assert.equal(saved[0].pointDecimationPercent, 49.5);
assert.equal(saved[0].accumulationSeconds, 10);
});
test('display stream validates fragmented header and sends bounded chunks', async () => {
const previous = globalThis.window; globalThis.window = {location: {origin: 'http://localhost'}};
try {
const wire = [78,80,68,49,8,0,0,0,82,82,70,50,1,2,3,4,0,0,0,0];
const chunks = [new Uint8Array(wire.slice(0,1)), new Uint8Array(wire.slice(1,7)),
new Uint8Array(wire.slice(7,10)), new Uint8Array(wire.slice(10))];
const sent = []; let request;
const fetcher = async (url, options) => {request = {url, ...options}; return new Response(new ReadableStream({
start(controller) {chunks.forEach(chunk => controller.enqueue(chunk)); controller.close();},
}), {headers: {'Content-Type': 'application/vnd.nodedc.point-display-stream'}});};
const total = await points.streamRecordedPointDisplay('/api/v1/observation-sessions/session-a/blueprint.rrd',
{...defaults, pointDecimationPercent: 49.5}, {applicationId: 'app', recordingId: 'rec'}, 'a'.repeat(32),
new AbortController().signal, chunk => sent.push(...chunk), fetcher, 'a'.repeat(64));
assert.equal(total, 20); assert.deepEqual(sent, [82,82,70,50,1,2,3,4]);
assert.equal(JSON.parse(request.body).point_decimation_percent, 49.5);
assert.equal(JSON.parse(request.body).source_generation, 'a'.repeat(64));
assert.match(request.url, /point-display\.rrd$/);
} finally {globalThis.window = previous;}
});
test('invalid, cross-origin and canceled streams never become active', async () => {
const previous = globalThis.window; globalThis.window = {location: {origin: 'http://localhost'}};
try {
const call = (url, signal, fetcher) => points.streamRecordedPointDisplay(url, {...defaults, pointDecimationPercent: 50},
{applicationId: 'app', recordingId: 'rec'}, 'b'.repeat(32), signal, () => assert.fail('must not publish'), fetcher);
const url = '/api/v1/observation-sessions/session-a/blueprint.rrd';
await assert.rejects(call('https://elsewhere.test'+url, new AbortController().signal, () => assert.fail('must not fetch')));
await assert.rejects(call(url, new AbortController().signal, async () => new Response('bad!', {
headers: {'Content-Type': 'application/vnd.nodedc.point-display-stream'}})), /Некорректный поток/);
await assert.rejects(call(url, new AbortController().signal, async () => new Response('NPD1', {
headers: {'Content-Type': 'application/vnd.nodedc.point-display-stream'}})), /не завершён/);
const abort = new AbortController(); abort.abort();
await assert.rejects(call(url, abort.signal, async () => new Response('RRF2', {
headers: {'Content-Type': 'application/vnd.nodedc.point-display-stream'}})), /Aborted/);
} finally {globalThis.window = previous;}
});
@@ -0,0 +1,55 @@
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/sessionOverviewSpatial.ts');
});
after(async () => { await server?.close(); });
test('comparison pins identity and changes geometry without requesting a camera reset', async () => {
const oldFetch = globalThis.fetch, oldWindow = globalThis.window;
const requests = [];
globalThis.window = { location: { origin: 'http://localhost:8000' } };
globalThis.fetch = async (url, options) => {
requests.push({ url: String(url), body: JSON.parse(options.body) });
return { ok: true, arrayBuffer: async () => new ArrayBuffer(8), headers: new Headers({ 'X-Overview-Visible-Points': '42' }) };
};
try {
const source = '/api/v1/observation-sessions/example/overview/scene.rrd?generation=' + 'a'.repeat(64);
const signal = new AbortController().signal;
for (const representation of ['original', 'corrected', 'original']) {
const result = await api.updateOverviewSpatial(source, 80, null, 1.5, signal, 'b'.repeat(64), representation);
assert.equal(result.eye, null);
assert.equal(result.visiblePoints, 42);
}
assert.deepEqual(requests.map(r => r.body.representation), ['original', 'corrected', 'original']);
for (const request of requests) {
assert.equal(request.body.mode, null);
assert.equal(request.body.comparison_generation, 'b'.repeat(64));
assert.equal(request.body.generation, 'a'.repeat(64));
assert.ok(request.url.endsWith('/overview/spatial'));
}
await assert.rejects(api.updateOverviewSpatial(source, null, null, 1, signal, null, 'corrected'), /недоступна/);
assert.equal(requests.length, 3);
} finally { globalThis.fetch = oldFetch; globalThis.window = oldWindow; }
});
test('comparison control is opt-in while default geometry and pinned planner versions are shared', async () => {
const source = await readFile(new URL('../src/components/observation/SessionOverviewScene.tsx', import.meta.url), 'utf8');
assert.match(source, /compareVersions=false/);
assert.match(source, /}, \[sourceUrl, retry\]\);/);
assert.match(source, /appliedMode.current === mode \? null : mode/);
assert.doesNotMatch(source, /key=\{representation\}|setRetry\([^\n]*representation/);
assert.match(source, /label="Версия облака"/);
assert.match(source, /setAppliedRepresentation\(comparison \? representation/);
assert.match(source, /setRepresentation\(data.default_representation/);
assert.match(source, /const comparison = metadata\?\.comparison/);
assert.match(source, /compareVersions && comparison/);
const planner = await readFile(new URL('../src/components/missions/MissionZonePreview.tsx', import.meta.url), 'utf8');
assert.doesNotMatch(planner, /compareVersions/);
assert.match(planner, /reference_generation=\$\{encodeURIComponent\(generation\)\}/);
const logic = await readFile(new URL('../src/core/missions/useMissionPlanner.ts', import.meta.url), 'utf8');
assert.match(logic, /setPinnedGeneration\(next.zone.generation\)/);
assert.match(logic, /setPinnedGeneration\(null\)/);
});
@@ -13,7 +13,8 @@ before(async () => {
after(async () => { await server?.close(); });
const render = (focused = false) => renderToStaticMarkup(createElement(SpatialScene, {
viewportRef: { current: null }, primaryFocused: focused, toolbar: null, renderer: null,
viewportRef: { current: null }, primaryFocused: focused, toolbar: null,
renderer: createElement('div', { 'data-testid': 'renderer' }, 'RENDERER'),
sourceControls: createElement('div', { className: focused ? 'scene-focus-exit' : 'scene-source-controls' }, 'SOURCE_CONTROLS'),
status: { label: 'Накопление данных', tone: 'neutral', message: 'Сканер неподвижен.' },
metrics: createElement('div', null, 'METRICS'),
@@ -37,6 +38,18 @@ test('focus exit remains viewport-owned outside the hidden information stack', (
assert.equal((markup.match(/SOURCE_CONTROLS/g) ?? []).length, 1);
});
test('normal and expanded scenes retain the renderer without mouse-navigation copy', async () => {
for (const focused of [false, true]) {
const markup = render(focused);
assert.match(markup, /data-testid="renderer">RENDERER/);
assert.doesNotMatch(markup, /scene-navigation-hint|Навигация по 3D-сцене|ЛКМ|ПКМ|Колесо/);
}
const source = await readFile(new URL('../../../packages/spatial-ui/src/SpatialScene.tsx', import.meta.url), 'utf8');
const css = await readFile(new URL('../../../packages/spatial-ui/src/observation.css', import.meta.url), 'utf8');
assert.doesNotMatch(source, /navigationReady|scene-navigation-hint/);
assert.doesNotMatch(css, /scene-navigation-hint/);
});
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');
@@ -48,3 +61,13 @@ test('scene layout uses flow, retains compact metrics and removes only the calib
assert.doesNotMatch(responsive, /\.scene-metrics\s*\{\s*display: none/);
assert.match(calibration, /\.xgrids-k1-spatial-controls \{[^}]*border: 0;/);
});
test('preparation status shares the source controls top axis on the opposite side', async () => {
const css = await readFile(new URL('../../../packages/spatial-ui/src/spatial.css', import.meta.url), 'utf8');
const stack = css.match(/\.scene-operation-status-stack \{[^}]*\}/)?.[0] ?? '';
assert.match(stack, /top: 0\.85rem/);
assert.match(stack, /right: 0\.85rem/);
assert.match(stack, /min-height: 2\.75rem/);
assert.match(stack, /align-content: center/);
assert.doesNotMatch(stack, /left:|bottom:/);
});