feat(control-station): add atomic recorded-session playback

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 17:51:24 +03:00
parent 007ff9fff2
commit e94c64eebd
35 changed files with 8907 additions and 302 deletions
@@ -0,0 +1,832 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decodeObservationSessionCatalog;
let decodeObservationSessionReplay;
let decodeObservationSessionPreparation;
let fetchObservationSessionCatalog;
let replayObservationSession;
let fetchObservationSessionPreparation;
let cancelObservationSessionPreparation;
let ObservationSessionApiError;
let ObservationSessionContractError;
let decodeObservationRecordedMediaManifest;
let createObservationReplayCoordinator;
let resolveObservationSessionReplay;
let waitForObservationReplayPreparation;
let storeObservationReplayPreparation;
let loadObservationReplayPreparation;
let observationSessionVisualState;
let observationSessionVisualLabel;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
decodeObservationSessionCatalog,
decodeObservationSessionReplay,
decodeObservationSessionPreparation,
fetchObservationSessionCatalog,
replayObservationSession,
fetchObservationSessionPreparation,
cancelObservationSessionPreparation,
ObservationSessionApiError,
ObservationSessionContractError,
decodeObservationRecordedMediaManifest,
} = await server.ssrLoadModule("/src/core/observation/sessionArchive.ts"));
({
createObservationReplayCoordinator,
resolveObservationSessionReplay,
waitForObservationReplayPreparation,
storeObservationReplayPreparation,
loadObservationReplayPreparation,
} = await server.ssrLoadModule(
"/src/core/observation/useObservationSessions.ts",
));
({ observationSessionVisualState, observationSessionVisualLabel } = await server.ssrLoadModule(
"/src/components/ObservationSessionSelect.tsx",
));
});
after(async () => {
await server?.close();
});
function session(overrides = {}) {
return {
id: "20260716T205632Z_viewer_live",
label: "K1 · наблюдение 16 июля",
started_at_utc: "2026-07-16T20:56:32.635Z",
completed_at_utc: "2026-07-16T21:20:43.379Z",
status: "ready",
modalities: ["point-cloud", "pose"],
duration_seconds: 1_450.744,
replayable: true,
...overrides,
};
}
function replay(overrides = {}) {
const sessionId = overrides.session_id ?? "session-20260716T205632Z";
const sourceUrl = overrides.source_url ??
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording.rrd`;
const sha256 = overrides.sha256 ?? "a".repeat(64);
return {
schema_version: "missioncore.observation-session-replay/v2",
launch: {
kind: "rerun-recording",
session_id: sessionId,
source_url: sourceUrl,
viewer_source_url: overrides.viewer_source_url ?? `${sourceUrl}?generation=${sha256}`,
media_type: "application/vnd.rerun.rrd",
timeline: "session_time",
timeline_start_seconds: 0,
timeline_end_seconds: 1_450.744,
seekable: true,
byte_length: 123_456,
sha256,
playback: { speed: 1, loop: false },
media_sources: [],
...overrides,
},
};
}
function preparation(overrides = {}) {
const sessionId = overrides.session_id ?? "session-20260716T205632Z";
return {
schema_version: "missioncore.observation-session-preparation/v1",
preparation: {
preparation_id: "prepare-20260717T131400Z",
session_id: sessionId,
state: "queued",
progress: null,
updated_at_utc: "2026-07-17T10:14:00Z",
status_url: `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording-preparation`,
cancellable: true,
...overrides,
},
};
}
const preparationEtag = '"prepare-20260717T131400Z"';
test("session catalog decodes canonical snake_case into a path-free camelCase model", () => {
const catalog = decodeObservationSessionCatalog({ items: [session()] });
assert.deepEqual(catalog.items, [{
id: "20260716T205632Z_viewer_live",
label: "K1 · наблюдение 16 июля",
startedAtUtc: "2026-07-16T20:56:32.635Z",
completedAtUtc: "2026-07-16T21:20:43.379Z",
status: "ready",
modalities: ["point-cloud", "pose"],
durationSeconds: 1_450.744,
replayable: true,
preparation: null,
}]);
assert.equal("raw_capture" in catalog.items[0], false);
assert.equal("path" in catalog.items[0], false);
});
test("session catalog exposes authoritative background preparation state", () => {
const catalog = decodeObservationSessionCatalog({
items: [session({
preparation: {
preparation_id: "prepare-catalog-1",
state: "exporting",
progress: 0.42,
updated_at_utc: "2026-07-17T10:14:00Z",
cancellable: true,
retryable: false,
},
})],
});
assert.deepEqual(catalog.items[0].preparation, {
preparationId: "prepare-catalog-1",
state: "exporting",
progress: 0.42,
updatedAtUtc: "2026-07-17T10:14:00Z",
cancellable: true,
retryable: false,
error: null,
});
});
test("saved-session rows use only authoritative ready, processing and error states", () => {
const makeDecoded = (state, overrides = {}) => decodeObservationSessionCatalog({
items: [session({
status: "interrupted",
preparation: state === null ? null : {
preparation_id: `prepare-${state}`,
state,
progress: state === "ready" ? 1 : 0.5,
updated_at_utc: "2026-07-17T10:14:00Z",
cancellable: ["queued", "validating", "exporting", "finalizing"].includes(state),
retryable: state === "failed",
...(state === "failed" ? { error: "export failed" } : {}),
},
...overrides,
})],
}).items[0];
assert.equal(observationSessionVisualState(makeDecoded("ready")), "ready");
for (const state of ["queued", "validating", "exporting", "finalizing"]) {
assert.equal(observationSessionVisualState(makeDecoded(state)), "processing");
}
assert.equal(observationSessionVisualState(makeDecoded("failed")), "error");
assert.equal(observationSessionVisualState(makeDecoded("cancelled")), "error");
assert.equal(
observationSessionVisualState(makeDecoded("failed", { status: "recording" })),
"error",
);
assert.equal(observationSessionVisualState(makeDecoded(null)), "error");
assert.equal(
observationSessionVisualState(makeDecoded(null, { status: "recording" })),
"processing",
);
assert.equal(
observationSessionVisualState(makeDecoded("ready", { replayable: false })),
"error",
);
assert.equal(
observationSessionVisualState(makeDecoded("ready"), { failed: true }),
"error",
);
assert.equal(
observationSessionVisualState(makeDecoded("failed"), { pending: true, failed: true }),
"error",
);
assert.equal(observationSessionVisualLabel("ready"), "Готово");
assert.equal(observationSessionVisualLabel("processing"), "Обработка");
assert.equal(observationSessionVisualLabel("error"), "Ошибка");
});
test("saved-session lamps use green or dim gray only, never warning or danger colors", async () => {
const css = await readFile(
new URL("../src/styles/observation-sessions.css", import.meta.url),
"utf8",
);
const start = css.indexOf(".observation-session-option i {");
const end = css.indexOf(".observation-session-option__state", start);
assert.notEqual(start, -1);
assert.notEqual(end, -1);
const lampRules = css.slice(start, end);
assert.match(lampRules, /data-session-visual-state="ready"[\s\S]*--nodedc-success-rgb/);
assert.match(lampRules, /data-session-visual-state="processing"[\s\S]*animation:/);
assert.match(lampRules, /data-session-visual-state="error"[\s\S]*--nodedc-text-muted/);
assert.match(lampRules, /data-session-visual-state="error"[\s\S]*opacity:\s*0\.48/);
assert.doesNotMatch(lampRules, /\b(?:warning|danger|yellow|red)\b/i);
});
test("session catalog sorts newest first and uses id as a deterministic tie-breaker", () => {
const catalog = decodeObservationSessionCatalog({
items: [
session({ id: "same-b", started_at_utc: "2026-07-16T19:00:00Z", completed_at_utc: null }),
session({ id: "newest", started_at_utc: "2026-07-17T00:00:00Z", completed_at_utc: null }),
session({ id: "same-a", started_at_utc: "2026-07-16T19:00:00Z", completed_at_utc: null }),
],
});
assert.deepEqual(catalog.items.map(({ id }) => id), ["newest", "same-a", "same-b"]);
});
test("session catalog rejects raw storage fields, unsafe ids and duplicate ids", () => {
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({ raw_capture: "sessions/private/mqtt.raw.k1mqtt" })],
}),
ObservationSessionContractError,
);
assert.throws(
() => decodeObservationSessionCatalog({ items: [session({ id: "../private" })] }),
ObservationSessionContractError,
);
assert.throws(
() => decodeObservationSessionCatalog({ items: [session(), session()] }),
/повторяющийся id/,
);
});
test("session catalog rejects invalid temporal, status and duration values", () => {
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({ started_at_utc: "16 July 2026" })],
}),
/ISO-датой/,
);
assert.throws(
() => decodeObservationSessionCatalog({ items: [session({ status: "complete" })] }),
/неизвестное состояние/,
);
assert.throws(
() => decodeObservationSessionCatalog({ items: [session({ duration_seconds: -1 })] }),
/неотрицательным числом/,
);
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({ completed_at_utc: "2026-07-16T19:00:00Z" })],
}),
/раньше времени начала/,
);
});
test("catalog API preserves HTTP detail without accepting a malformed success body", async () => {
await assert.rejects(
fetchObservationSessionCatalog({
fetcher: async () => new Response(JSON.stringify({ detail: "storage unavailable" }), {
status: 503,
headers: { "Content-Type": "application/json" },
}),
}),
(error) => error instanceof ObservationSessionApiError &&
error.status === 503 && error.message === "storage unavailable",
);
await assert.rejects(
fetchObservationSessionCatalog({
fetcher: async () => new Response(JSON.stringify({ items: "not-an-array" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
}),
ObservationSessionContractError,
);
});
test("replay API accepts only a same-origin seekable recording descriptor", async () => {
const calls = [];
const launch = await replayObservationSession("session-20260716T205632Z", {
fetcher: async (input, init) => {
calls.push({ input: String(input), init });
return new Response(JSON.stringify(replay()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(calls.length, 1);
assert.equal(calls[0].input, "/api/v1/observation-sessions/session-20260716T205632Z/replay");
assert.equal(calls[0].init.method, "POST");
assert.equal(launch.kind, "ready");
assert.equal(launch.launch.timeline, "session_time");
assert.equal(
launch.launch.sourceUrl,
"/api/v1/observation-sessions/session-20260716T205632Z/recording.rrd",
);
await assert.rejects(
replayObservationSession("../private", { fetcher: async () => new Response(null) }),
ObservationSessionContractError,
);
});
test("preparation contract is strict, session-scoped and same-origin", async () => {
const sessionId = "session-20260716T205632Z";
const decoded = decodeObservationSessionPreparation(preparation(), sessionId);
assert.equal(decoded.sessionId, sessionId);
assert.equal(decoded.state, "queued");
assert.equal(decoded.progress, null);
assert.throws(
() => decodeObservationSessionPreparation(
preparation({ status_url: "https://invalid.test/status" }),
sessionId,
),
/канонический same-origin/,
);
assert.throws(
() => decodeObservationSessionPreparation(preparation({ debug_path: "/tmp/raw" }), sessionId),
/неизвестные поля/,
);
assert.throws(
() => decodeObservationSessionPreparation(preparation({ progress: 1.1 }), sessionId),
/недопустимое число/,
);
await assert.rejects(
replayObservationSession(sessionId, {
fetcher: async () => new Response(JSON.stringify(preparation()), {
status: 202,
headers: { "Content-Type": "application/json" },
}),
}),
/preparation ETag/,
);
});
test("202 preparation polls through explicit phases and reveals launch only when ready", async () => {
const sessionId = "session-20260716T205632Z";
const calls = [];
const phases = [];
let poll = 0;
let clock = 0;
let previousViewerMounted = true;
const launch = await resolveObservationSessionReplay(sessionId, {
signal: new AbortController().signal,
requestTimeoutMs: 1_000,
heartbeatStallMs: 5_000,
maximumWaitMs: 10_000,
initialPollIntervalMs: 50,
maximumPollIntervalMs: 50,
now: () => clock,
sleep: async (milliseconds) => {
assert.equal(previousViewerMounted, true);
clock += milliseconds;
},
onUpdate: (value) => {
assert.equal(previousViewerMounted, true);
phases.push([value.state, value.progress]);
},
fetcher: async (input, init) => {
calls.push([String(input), init.method, new Headers(init.headers).get("If-Match")]);
if (init.method === "POST") {
return new Response(JSON.stringify(preparation()), {
status: 202,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
});
}
poll += 1;
if (poll === 1) {
return new Response(JSON.stringify(preparation({
state: "exporting",
progress: 0.5,
updated_at_utc: "2026-07-17T10:14:01Z",
})), {
status: 202,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
});
}
return new Response(JSON.stringify(replay({ session_id: sessionId })), {
status: 200,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
});
},
});
// This is the point where App is allowed to unmount the old viewer.
previousViewerMounted = false;
assert.equal(launch.sessionId, sessionId);
assert.deepEqual(phases, [["queued", null], ["exporting", 0.5]]);
assert.deepEqual(calls.map((entry) => entry[1]), ["POST", "GET", "GET"]);
assert.deepEqual(calls.map((entry) => entry[2]), [null, preparationEtag, preparationEtag]);
});
test("status-ready response must match the preparation ETag before launch is accepted", async () => {
const decoded = decodeObservationSessionPreparation(
preparation({ state: "finalizing", progress: 0.95 }),
"session-20260716T205632Z",
);
let receivedIfMatch;
await assert.rejects(
fetchObservationSessionPreparation(decoded, {
fetcher: async (_input, init) => {
receivedIfMatch = new Headers(init.headers).get("If-Match");
return new Response(JSON.stringify(replay()), {
status: 200,
headers: { "Content-Type": "application/json", ETag: '"replacement-job"' },
});
},
}),
/актуальный preparation ETag/,
);
assert.equal(receivedIfMatch, preparationEtag);
});
test("preparation cancellation is conditional on the same preparation ETag", async () => {
const decoded = decodeObservationSessionPreparation(
preparation({ state: "exporting", progress: 0.5 }),
"session-20260716T205632Z",
);
let request;
const result = await cancelObservationSessionPreparation(decoded, {
fetcher: async (input, init) => {
request = {
input: String(input),
method: init.method,
ifMatch: new Headers(init.headers).get("If-Match"),
};
return new Response(null, { status: 204 });
},
});
assert.equal(result, null);
assert.deepEqual(request, {
input: decoded.statusUrl,
method: "DELETE",
ifMatch: preparationEtag,
});
});
test("preparation polling fails explicitly when the server heartbeat stalls", async () => {
const decoded = decodeObservationSessionPreparation(
preparation({ state: "exporting", progress: 0.1 }),
"session-20260716T205632Z",
);
let clock = 0;
await assert.rejects(
waitForObservationReplayPreparation(decoded, {
signal: new AbortController().signal,
requestTimeoutMs: 1_000,
heartbeatStallMs: 250,
maximumWaitMs: 5_000,
initialPollIntervalMs: 150,
maximumPollIntervalMs: 150,
now: () => clock,
sleep: async (milliseconds) => { clock += milliseconds; },
fetcher: async () => new Response(JSON.stringify(preparation({
state: "exporting",
progress: 0.1,
})), {
status: 202,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
}),
}),
/перестала обновляться/,
);
});
test("queued preparation can wait behind the bounded backend worker without false heartbeat failure", async () => {
const decoded = decodeObservationSessionPreparation(
preparation({ state: "queued", progress: 0 }),
"session-20260716T205632Z",
);
let clock = 0;
let polls = 0;
const launch = await waitForObservationReplayPreparation(decoded, {
signal: new AbortController().signal,
requestTimeoutMs: 1_000,
heartbeatStallMs: 250,
maximumWaitMs: 5_000,
initialPollIntervalMs: 150,
maximumPollIntervalMs: 150,
now: () => clock,
sleep: async (milliseconds) => { clock += milliseconds; },
fetcher: async () => {
polls += 1;
if (polls <= 2) {
return new Response(JSON.stringify(preparation({ state: "queued", progress: 0 })), {
status: 202,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
});
}
return new Response(JSON.stringify(replay()), {
status: 200,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
});
},
});
assert.equal(clock, 450);
assert.equal(launch.sessionId, "session-20260716T205632Z");
});
test("terminal preparation failure is explicit and retryable", async () => {
const sessionId = "session-20260716T205632Z";
const response = await replayObservationSession(sessionId, {
fetcher: async () => new Response(JSON.stringify(preparation({
state: "failed",
progress: 0.4,
cancellable: false,
retryable: true,
error: "Повреждён индекс исходной записи.",
})), {
status: 409,
headers: { "Content-Type": "application/json", ETag: preparationEtag },
}),
});
assert.equal(response.kind, "preparing");
assert.equal(response.preparation.retryable, true);
assert.equal(response.preparation.error, "Повреждён индекс исходной записи.");
});
test("pending preparation survives reload only through its strict canonical contract", () => {
const values = new Map();
const storage = {
get length() { return values.size; },
clear: () => values.clear(),
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => values.delete(key),
setItem: (key, value) => values.set(key, String(value)),
};
const decoded = decodeObservationSessionPreparation(
preparation({ state: "exporting", progress: 0.7 }),
"session-20260716T205632Z",
);
storeObservationReplayPreparation(decoded, storage);
assert.deepEqual(loadObservationReplayPreparation(storage), decoded);
const key = storage.key(0);
storage.setItem(key, JSON.stringify({ ...preparation(), raw_path: "/private/raw" }));
assert.equal(loadObservationReplayPreparation(storage), null);
assert.equal(storage.length, 0);
});
test("replay API forwards cancellation and preserves AbortError semantics", async () => {
const controller = new AbortController();
let receivedSignal;
const pending = replayObservationSession("session-20260716T205632Z", {
signal: controller.signal,
fetcher: async (_input, init) => {
receivedSignal = init.signal;
return await new Promise((_resolve, reject) => {
init.signal.addEventListener("abort", () => {
reject(new DOMException("cancelled", "AbortError"));
}, { once: true });
});
},
});
controller.abort();
assert.equal(receivedSignal, controller.signal);
await assert.rejects(pending, (error) => error?.name === "AbortError");
});
test("replay coordinator makes rapid saved-session selection latest-request-wins", () => {
const coordinator = createObservationReplayCoordinator();
const first = coordinator.begin();
assert.equal(first.isCurrent(), true);
const second = coordinator.begin();
assert.equal(first.signal.aborted, true);
assert.equal(first.isCurrent(), false);
assert.equal(first.finish(), false);
assert.equal(second.isCurrent(), true);
assert.equal(second.finish(), true);
assert.equal(second.isCurrent(), false);
const third = coordinator.begin();
coordinator.cancel();
assert.equal(third.signal.aborted, true);
assert.equal(third.isCurrent(), false);
});
test("switching saved sessions aborts only local polling and never cancels shared backend work", async () => {
const hookSource = await readFile(
new URL("../src/core/observation/useObservationSessions.ts", import.meta.url),
"utf8",
);
const selectorSource = await readFile(
new URL("../src/components/ObservationSessionSelect.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(hookSource, /cancelObservationSessionPreparation|method:\s*["']DELETE/);
assert.doesNotMatch(selectorSource, /cancelReplay|>\s*Отменить\s*</);
});
test("replay descriptor rejects path leaks, mismatched sessions and non-seekable data", () => {
assert.throws(
() => decodeObservationSessionReplay(replay({ source_url: "file:///private/session.rrd" })),
/same-origin/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({ source_url: "/api/v1/observation-sessions/other/recording.rrd" })),
/same-origin/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({ seekable: false })),
/seekable/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({
viewer_source_url: "/api/v1/observation-sessions/session-20260716T205632Z/recording.rrd",
})),
/канонически привязан/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({
viewer_source_url: `https://foreign.test/recording.rrd?generation=${"a".repeat(64)}`,
})),
/канонически привязан/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({ raw_path: "/private/raw.k1mqtt" })),
/неизвестные поля/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({ byte_length: Number.MAX_SAFE_INTEGER + 1 })),
/недопустимое число/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({ playback: { speed: 101, loop: false } })),
/недопустимое число/,
);
});
test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest", () => {
const sessionId = "session-20260716T205632Z";
const source = {
id: "recorded.camera.5a5a5a5a5a5a5a5a",
label: "Записанная камера 1",
modality: "video",
manifest_url: `/api/v1/observation-sessions/${sessionId}/media/recorded-video-5a5a5a5a5a5a5a5a/manifest`,
manifest_generation_sha256: "c".repeat(64),
byte_length: 3_266,
media_type: "video/mp4",
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
seekable: true,
synchronization: "host-arrival-best-effort",
};
const decoded = decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [source],
}));
assert.equal(
decoded.viewerSourceUrl,
`/api/v1/observation-sessions/${sessionId}/recording.rrd?generation=${"a".repeat(64)}`,
);
assert.equal(decoded.mediaSources.length, 1);
assert.equal(decoded.mediaSources[0].id, source.id);
assert.equal(decoded.mediaSources[0].manifestUrl, source.manifest_url);
assert.equal(
decoded.mediaSources[0].manifestGenerationSha256,
source.manifest_generation_sha256,
);
const manifest = decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
source_id: source.id,
generation_sha256: "c".repeat(64),
byte_length: 3_266,
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
synchronization: "host-arrival-best-effort",
epochs: [{
ordinal: 1,
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: source.manifest_url.replace("/manifest", "/epochs/1/init.mp4"),
init_byte_length: 128,
init_sha256: "a".repeat(64),
segment_count: 12,
segment_url_prefix: source.manifest_url.replace("/manifest", "/epochs/1/segments/"),
segments: Array.from({ length: 12 }, (_, index) => ({
sequence: index + 1,
url: source.manifest_url.replace("/manifest", `/epochs/1/segments/${index + 1}.m4s`),
byte_length: 256 + index,
sha256: "b".repeat(64),
})),
}],
}, decoded.mediaSources[0]);
assert.equal(manifest.epochs[0].segmentCount, 12);
assert.equal(manifest.epochs[0].segments.length, 12);
assert.equal(manifest.epochs[0].timelineStartSeconds, 0.25);
assert.throws(
() => decodeObservationRecordedMediaManifest({
...{
schema_version: "missioncore.observation-recorded-media/v1",
source_id: source.id,
generation_sha256: "c".repeat(64),
byte_length: 3_266,
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
synchronization: "host-arrival-best-effort",
epochs: [],
},
}, decoded.mediaSources[0]),
/несовместим/,
);
assert.throws(
() => decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
source_id: source.id,
generation_sha256: "c".repeat(64),
byte_length: 3_267,
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
synchronization: "host-arrival-best-effort",
epochs: [],
}, decoded.mediaSources[0]),
/несовместим|launch descriptor/,
);
});
test("recorded camera contracts reject foreign origins, path escapes and unknown fields", () => {
const sessionId = "session-20260716T205632Z";
const base = {
id: "recorded.camera.5a5a5a5a5a5a5a5a",
label: "Записанная камера 1",
modality: "video",
manifest_url: `/api/v1/observation-sessions/${sessionId}/media/recorded-video-5a5a5a5a5a5a5a5a/manifest`,
manifest_generation_sha256: "c".repeat(64),
byte_length: 384,
media_type: "video/mp4",
timeline_start_seconds: 0,
timeline_end_seconds: 20,
seekable: true,
synchronization: "host-arrival-best-effort",
};
assert.throws(
() => decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [{ ...base, manifest_url: "https://invalid.test/private" }],
})),
/same-origin/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [{ ...base, path: "/private/archive" }],
})),
/неизвестные поля/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [{ ...base, manifest_generation_sha256: undefined }],
})),
/immutable generation/,
);
assert.throws(
() => decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [{ ...base, manifest_generation_sha256: "not-a-digest" }],
})),
/immutable generation/,
);
const decoded = decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 20,
media_sources: [base],
}));
assert.throws(
() => decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
source_id: base.id,
generation_sha256: "c".repeat(64),
byte_length: 384,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
synchronization: "host-arrival-best-effort",
epochs: [{
ordinal: 1,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: "file:///private/init.mp4",
init_byte_length: 128,
init_sha256: "a".repeat(64),
segment_count: 1,
segment_url_prefix: "file:///private/segments/",
segments: [{
sequence: 1,
url: "file:///private/segments/1.m4s",
byte_length: 256,
sha256: "b".repeat(64),
}],
}],
}, decoded.mediaSources[0]),
/небезопасный API URL/,
);
});
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { createServer } from "vite";
let server;
@@ -11,6 +13,20 @@ let shouldRestartObservationSource;
let consumeCameraLeaseRetry;
let resetCameraLeaseRetryBudget;
let initialObservationWindowRect;
let ObservationTimeline;
let shouldCaptureWorkspacePointer;
let normalizeTimelineRange;
let timelineOffsetSeconds;
let formatTimelineDuration;
let normalizeAccumulationSeconds;
let formatAccumulationDuration;
let resolveRerunSourceUrl;
let resolveRecordedBlueprintUrl;
let fetchRecordedBlueprintRrd;
let isRecordedPlaybackFullyBuffered;
let recordedObservationSources;
let selectRecordedMediaEpoch;
let recordedMediaLocalTime;
before(async () => {
server = await createServer({
@@ -30,9 +46,32 @@ before(async () => {
({ consumeCameraLeaseRetry, resetCameraLeaseRetryBudget } = await server.ssrLoadModule(
"/src/components/MseFmp4WebSocketPlayer.tsx",
));
({ initialObservationWindowRect } = await server.ssrLoadModule(
({ initialObservationWindowRect, shouldCaptureWorkspacePointer } = await server.ssrLoadModule(
"/src/components/FloatingObservationWindow.tsx",
));
({
ObservationTimeline,
normalizeTimelineRange,
timelineOffsetSeconds,
formatTimelineDuration,
normalizeAccumulationSeconds,
formatAccumulationDuration,
} =
await server.ssrLoadModule("/src/components/ObservationTimeline.tsx"));
({
resolveRerunSourceUrl,
resolveRecordedBlueprintUrl,
fetchRecordedBlueprintRrd,
isRecordedPlaybackFullyBuffered,
} = await server.ssrLoadModule(
"/src/components/RerunViewport.tsx",
));
({ recordedObservationSources } = await server.ssrLoadModule(
"/src/core/observation/recordedObservationSources.ts",
));
({ selectRecordedMediaEpoch, recordedMediaLocalTime } = await server.ssrLoadModule(
"/src/components/RecordedFmp4Player.tsx",
));
});
test("observation camera windows tile from the bottom-right above the live timeline", () => {
@@ -75,6 +114,251 @@ test("observation camera tiling remains in bounds on a constrained viewport", ()
assert.ok(rects[1].y + rects[1].height <= rects[0].y);
});
test("floating observation interaction captures resize and movable header pointers", () => {
const target = (matches) => ({ closest: (selector) => matches.includes(selector) });
assert.equal(
shouldCaptureWorkspacePointer(0, target([".nodedc-workspace-window__resize"])),
true,
);
assert.equal(
shouldCaptureWorkspacePointer(0, target([".nodedc-workspace-window__head"])),
true,
);
assert.equal(
shouldCaptureWorkspacePointer(
0,
target([".nodedc-workspace-window__head", "button, input, select, textarea, a"]),
),
false,
);
assert.equal(
shouldCaptureWorkspacePointer(2, target([".nodedc-workspace-window__resize"])),
false,
);
});
test("recorded observation timeline clamps relative seek time without epoch precision in the UI", () => {
const range = normalizeTimelineRange({
min: 0,
max: 12_500_000_000,
});
assert.ok(range);
assert.equal(timelineOffsetSeconds(range, range.min + 2_250_000_000), 2.25);
assert.equal(timelineOffsetSeconds(range, range.min - 1), 0);
assert.equal(timelineOffsetSeconds(range, range.max + 1), 12.5);
assert.equal(formatTimelineDuration(62.125), "01:02.125");
});
test("recorded observation timeline rejects empty and non-finite ranges", () => {
assert.equal(normalizeTimelineRange(null), null);
assert.equal(normalizeTimelineRange({ min: 10, max: 10 }), null);
assert.equal(normalizeTimelineRange({ min: Number.NaN, max: 10 }), null);
});
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(Number.NaN), 0);
assert.equal(formatAccumulationDuration(0), "Кадр");
assert.equal(formatAccumulationDuration(12), "12 с");
});
test("spatial timeline renders synchronized accumulation and playback controls", () => {
const markup = renderToStaticMarkup(createElement(ObservationTimeline, {
active: true,
sourceCount: 2,
mode: "recorded",
seekable: true,
rangeNs: { min: 0, max: 20_000_000_000 },
currentNs: 5_000_000_000,
accumulationSeconds: 12,
onAccumulationChange: () => undefined,
onAccumulationCommit: () => undefined,
onSeek: () => undefined,
}));
assert.match(markup, /data-accumulation="true"/);
assert.match(markup, /Накопление/);
assert.match(markup, /aria-label="Окно накопления облака точек"/);
assert.match(markup, /aria-valuetext="12 с"/);
assert.match(markup, /aria-label="Позиция воспроизведения"/);
assert.equal((markup.match(/type="range"/g) ?? []).length, 2);
});
test("Rerun expands only root-relative session recordings onto the current origin", () => {
assert.equal(
resolveRerunSourceUrl(
" /api/v1/observation-sessions/session-1/recording.rrd ",
"http://127.0.0.1:5174",
),
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/recording.rrd",
);
assert.equal(
resolveRerunSourceUrl("rerun+http://127.0.0.1:9877/proxy", "http://127.0.0.1:5174"),
"rerun+http://127.0.0.1:9877/proxy",
);
assert.equal(
resolveRerunSourceUrl("//different-authority.invalid/session.rrd", "http://127.0.0.1:5174"),
"//different-authority.invalid/session.rrd",
);
});
test("recorded blueprint endpoint is derived only from canonical same-origin RRD sources", () => {
assert.equal(
resolveRecordedBlueprintUrl(
"/api/v1/observation-sessions/session-1/recording.rrd",
"http://127.0.0.1:5174",
),
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
);
assert.equal(
resolveRecordedBlueprintUrl(
"https://outside.invalid/api/v1/observation-sessions/session-1/recording.rrd",
"http://127.0.0.1:5174",
),
null,
);
assert.equal(
resolveRecordedBlueprintUrl(
"/api/v1/observation-sessions/../recording.rrd",
"http://127.0.0.1:5174",
),
null,
);
});
test("recorded replay becomes ready only after the complete declared timeline is buffered", () => {
assert.equal(isRecordedPlaybackFullyBuffered(null, 20), false);
assert.equal(
isRecordedPlaybackFullyBuffered({ min: 0, max: 19_500_000_000 }, 20),
false,
);
assert.equal(
isRecordedPlaybackFullyBuffered({ min: 0, max: 19_999_500_000 }, 20),
true,
);
assert.equal(
isRecordedPlaybackFullyBuffered({ min: 0, max: 1 }, undefined),
true,
);
assert.equal(
isRecordedPlaybackFullyBuffered({ min: 0, max: 20_000_000_000 }, Number.NaN),
false,
);
});
test("recorded blueprint fetch is bounded, strict and sends only display settings", async () => {
const calls = [];
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
const result = await fetchRecordedBlueprintRrd(
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
{
accumulationSeconds: 24,
showGrid: false,
showPoints: true,
showTrajectory: false,
pointSize: 4.5,
palette: "custom",
customColor: "#35d7c1",
},
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
{
origin: "http://127.0.0.1:5174",
fetcher: async (input, init) => {
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
return new Response(payload, {
status: 200,
headers: { "Content-Type": "application/vnd.rerun.rrd" },
});
},
},
);
assert.deepEqual([...result], [...payload]);
assert.equal(calls[0].init.method, "POST");
assert.equal(calls[0].init.credentials, "same-origin");
assert.deepEqual(calls[0].body, {
application_id: "nodedc_mission_core_recorded",
recording_id: "recording-001",
accumulation_seconds: 24,
show_grid: false,
show_points: true,
show_trajectory: false,
point_size: 4.5,
palette: "custom",
custom_color: "#35d7c1",
});
await assert.rejects(
fetchRecordedBlueprintRrd(
"https://outside.invalid/api/v1/observation-sessions/session-1/blueprint.rrd",
{
accumulationSeconds: 24,
showGrid: false,
showPoints: true,
showTrajectory: false,
pointSize: 4.5,
palette: "custom",
customColor: "#35d7c1",
},
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
{ origin: "http://127.0.0.1:5174", fetcher: async () => new Response(payload) },
),
/Unsafe recorded blueprint request/,
);
});
test("recorded replay creates an isolated source catalog without live device bindings", () => {
const sources = recordedObservationSources({
kind: "rerun-recording",
sessionId: "session-1",
sourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd",
viewerSourceUrl: `/api/v1/observation-sessions/session-1/recording.rrd?generation=${"a".repeat(64)}`,
mediaType: "application/vnd.rerun.rrd",
timeline: "session_time",
timelineStartSeconds: 0,
timelineEndSeconds: 20,
seekable: true,
byteLength: 123,
sha256: "a".repeat(64),
playback: { speed: 1, loop: false },
mediaSources: [{
id: "recorded.camera.abc123",
label: "Записанная камера 1",
modality: "video",
manifestUrl: "/api/v1/observation-sessions/session-1/media/recorded-video-abc123/manifest",
manifestGenerationSha256: "c".repeat(64),
byteLength: 1_024,
mediaType: "video/mp4",
timelineStartSeconds: 0.25,
timelineEndSeconds: 20,
seekable: true,
synchronization: "host-arrival-best-effort",
}],
});
assert.deepEqual(sources.map(({ modality }) => modality), ["point-cloud", "video"]);
assert.equal(sources[1].delivery.kind, "recorded-fmp4-manifest");
assert.equal(sources[1].delivery.manifestGenerationSha256, "c".repeat(64));
assert.deepEqual(sources[1].binding, {});
assert.equal(sources.some(({ provider }) => provider.pluginId.includes("xgrids")), false);
assert.equal(JSON.stringify(sources).includes("192.168"), false);
});
test("recorded camera epoch selection and shared-clock offset are deterministic", () => {
const epochs = [
{ ordinal: 1, timelineStartSeconds: 0.25, timelineEndSeconds: 9 },
{ ordinal: 2, timelineStartSeconds: 10, timelineEndSeconds: 20 },
];
assert.equal(selectRecordedMediaEpoch(epochs, 0), null);
assert.equal(selectRecordedMediaEpoch(epochs, 0.25).ordinal, 1);
assert.equal(selectRecordedMediaEpoch(epochs, 9), epochs[0]);
assert.equal(selectRecordedMediaEpoch(epochs, 9.9), null);
assert.equal(selectRecordedMediaEpoch(epochs, 10).ordinal, 2);
assert.equal(recordedMediaLocalTime(10, 12.5), 2.5);
assert.equal(recordedMediaLocalTime(10, 8), 0);
assert.equal(recordedMediaLocalTime(10, 30, 4), 4);
});
after(async () => {
await server?.close();
});
@@ -0,0 +1,303 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchVerifiedRecordedMediaArchive;
let recordedMediaPresentationState;
let recordedMediaSeekableCoverage;
let appendRecordedMediaBuffer;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
fetchVerifiedRecordedMediaArchive,
recordedMediaPresentationState,
recordedMediaSeekableCoverage,
appendRecordedMediaBuffer,
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
});
after(async () => {
await server?.close();
});
function digest(payload) {
return createHash("sha256").update(payload).digest("hex");
}
function fixture() {
const manifestUrl = "/api/v1/observation-sessions/session-1/media/camera-1/manifest";
const init = Buffer.from("canonical-init");
const first = Buffer.from("canonical-first-fragment");
const second = Buffer.from("canonical-second-fragment");
const generation = "a".repeat(64);
const segmentPrefix = manifestUrl.replace("/manifest", "/epochs/1/segments/");
const manifest = {
schema_version: "missioncore.observation-recorded-media/v2",
source_id: "recorded.camera.camera-1",
generation_sha256: generation,
byte_length: init.byteLength + first.byteLength + second.byteLength,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
synchronization: "host-arrival-best-effort",
epochs: [{
ordinal: 1,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: manifestUrl.replace("/manifest", "/epochs/1/init.mp4"),
init_byte_length: init.byteLength,
init_sha256: digest(init),
segment_count: 2,
segment_url_prefix: segmentPrefix,
segments: [first, second].map((payload, index) => ({
sequence: index + 1,
url: `${segmentPrefix}${index + 1}.m4s`,
byte_length: payload.byteLength,
sha256: digest(payload),
})),
}],
};
const source = {
id: manifest.source_id,
label: "Записанная камера",
modality: "video",
manifestUrl,
manifestGenerationSha256: generation,
byteLength: manifest.byte_length,
mediaType: "video/mp4",
timelineStartSeconds: 0,
timelineEndSeconds: 20,
seekable: true,
synchronization: "host-arrival-best-effort",
};
return { source, manifest, generation, init, first, second };
}
function jsonResponse(payload, generation) {
return new Response(JSON.stringify(payload), {
status: 200,
headers: {
"Content-Type": "application/json",
ETag: `"sha256:${generation}"`,
},
});
}
function mediaResponse(payload, sha = digest(payload), length = payload.byteLength) {
return new Response(payload, {
status: 200,
headers: {
"Content-Length": String(length),
ETag: `"sha256:${sha}"`,
},
});
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function flushAsync() {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
}
test("recorded camera archive stays pending until every canonical byte is verified", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const finalSegment = deferred();
const requested = [];
let manifestRequests = 0;
const fetcher = async (input, request = {}) => {
const url = String(input);
requested.push(url);
const headers = new Headers(request.headers);
if (url === source.manifestUrl) {
manifestRequests += 1;
assert.equal(headers.get("If-Match"), `"sha256:${generation}"`);
return jsonResponse(manifest, generation);
}
const epoch = manifest.epochs[0];
if (url === epoch.init_url) {
assert.equal(headers.get("If-Match"), `"sha256:${epoch.init_sha256}"`);
return mediaResponse(init);
}
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) return finalSegment.promise;
throw new Error(`Unexpected URL ${url}`);
};
let settled = false;
const archivePromise = fetchVerifiedRecordedMediaArchive(source, { fetcher })
.then((archive) => {
settled = true;
return archive;
});
await flushAsync();
assert.equal(settled, false, "init and a partial segment prefix must not publish the camera");
assert.deepEqual(requested.slice(0, 4), [
source.manifestUrl,
manifest.epochs[0].init_url,
manifest.epochs[0].segments[0].url,
manifest.epochs[0].segments[1].url,
]);
finalSegment.resolve(mediaResponse(second));
const archive = await archivePromise;
assert.equal(manifestRequests, 2, "the immutable generation is revalidated after transfer");
assert.equal(archive.byteLength, init.byteLength + first.byteLength + second.byteLength);
assert.equal(archive.epochs[0].segments.length, 2);
});
test("first camera manifest request is launch-generation bound and rejects replacement", async () => {
const { source, manifest, generation } = fixture();
const replacementGeneration = "d".repeat(64);
let requests = 0;
const fetcher = async (input, request = {}) => {
requests += 1;
assert.equal(String(input), source.manifestUrl);
assert.equal(
new Headers(request.headers).get("If-Match"),
`"sha256:${generation}"`,
);
return jsonResponse(
{ ...manifest, generation_sha256: replacementGeneration },
replacementGeneration,
);
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
/несовместим|заменён/,
);
assert.equal(requests, 1);
});
test("recorded camera archive fails closed on truncation despite plausible headers", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const fetcher = async (input) => {
const url = String(input);
if (url === source.manifestUrl) return jsonResponse(manifest, generation);
const epoch = manifest.epochs[0];
if (url === epoch.init_url) return mediaResponse(init);
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) {
return mediaResponse(second.subarray(0, second.byteLength - 1), digest(second), second.byteLength);
}
throw new Error(`Unexpected URL ${url}`);
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
/усечён/,
);
});
test("recorded camera archive fails closed when bytes are replaced under an old ETag", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const replacement = Buffer.from(second.map((value) => value ^ 0xff));
assert.equal(replacement.byteLength, second.byteLength);
const fetcher = async (input) => {
const url = String(input);
if (url === source.manifestUrl) return jsonResponse(manifest, generation);
const epoch = manifest.epochs[0];
if (url === epoch.init_url) return mediaResponse(init);
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) {
return mediaResponse(replacement, digest(second), second.byteLength);
}
throw new Error(`Unexpected URL ${url}`);
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
/SHA-256/,
);
});
test("camera presentation gate opens only for the completely appended selected epoch", () => {
assert.equal(recordedMediaPresentationState("loading", null, "g1", false), "loading");
assert.equal(recordedMediaPresentationState("ready", null, "g1", false), "loading");
assert.equal(recordedMediaPresentationState("ready", "g2", "g1", false), "loading");
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false), "ready");
assert.equal(recordedMediaPresentationState("ready", "g1", null, true), "waiting");
assert.equal(recordedMediaPresentationState("error", "g1", "g1", false), "error");
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false, "loading"), "loading");
assert.equal(recordedMediaPresentationState("ready", "g1", null, true, "loading"), "loading");
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false, "error"), "error");
});
test("decoded duration and seekable range cover the complete declared epoch", () => {
assert.equal(recordedMediaSeekableCoverage(20, 20, 20), true);
assert.equal(recordedMediaSeekableCoverage(19, 19, 20), true);
assert.equal(recordedMediaSeekableCoverage(18.99, 20, 20), false);
assert.equal(recordedMediaSeekableCoverage(20, 18.99, 20), false);
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1), true);
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
});
test("aborting SourceBuffer append removes every temporary listener", async () => {
const listeners = new Map();
const sourceBuffer = {
addEventListener(type, listener) {
const bucket = listeners.get(type) ?? new Set();
bucket.add(listener);
listeners.set(type, bucket);
},
removeEventListener(type, listener) {
listeners.get(type)?.delete(listener);
},
appendBuffer() {},
};
const abort = new AbortController();
const pending = appendRecordedMediaBuffer(
sourceBuffer,
new ArrayBuffer(8),
abort.signal,
);
assert.equal(listeners.get("updateend")?.size, 1);
assert.equal(listeners.get("error")?.size, 1);
abort.abort();
await assert.rejects(pending, (error) => error?.name === "AbortError");
assert.equal(listeners.get("updateend")?.size, 0);
assert.equal(listeners.get("error")?.size, 0);
});
test("verified camera archives remain immutable for safe player remount", async () => {
const source = await readFile(
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(source, /\.init\s*=\s*new ArrayBuffer/);
assert.doesNotMatch(source, /\.segments\.length\s*=\s*0/);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
"utf8",
);
assert.match(
css,
/\.recorded-media-player:not\(\[data-state="ready"\]\) \.observation-media__asset\s*\{[^}]*visibility:\s*hidden/s,
);
assert.match(
css,
/\.recorded-media-player__notice\s*\{[^}]*inset:\s*0;[^}]*background:\s*#070809/s,
);
});
@@ -0,0 +1,175 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let admission;
let rerunPresentationStatus;
let recordedMediaPresentationState;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
admission = await server.ssrLoadModule(
"/src/core/observation/recordedSessionAdmission.ts",
);
({ rerunPresentationStatus } = await server.ssrLoadModule(
"/src/components/RerunViewport.tsx",
));
({ recordedMediaPresentationState } = await server.ssrLoadModule(
"/src/components/RecordedFmp4Player.tsx",
));
});
after(async () => {
await server?.close();
});
function camera(phase, byteLength = 1_024) {
return { phase, byteLength, message: null };
}
test("recorded session admits RRD and every declared camera as one atomic generation", () => {
const ids = ["camera.left", "camera.right"];
const oneCamera = {
"camera.left": camera("ready"),
"camera.right": camera("pending"),
};
const partialGate = admission.recordedSessionAdmissionPhase("ready", ids, oneCamera);
assert.equal(partialGate, "loading");
assert.equal(rerunPresentationStatus("ready", partialGate, true), "loading");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, partialGate),
"loading",
);
const completeGate = admission.recordedSessionAdmissionPhase("ready", ids, {
...oneCamera,
"camera.right": camera("ready"),
});
assert.equal(completeGate, "ready");
assert.equal(rerunPresentationStatus("ready", completeGate, true), "ready");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, completeGate),
"ready",
);
});
test("any RRD or camera failure closes the complete recorded session", () => {
const ids = ["camera.left", "camera.right"];
const cameras = {
"camera.left": camera("ready"),
"camera.right": camera("error"),
};
assert.equal(admission.recordedSessionAdmissionPhase("ready", ids, cameras), "error");
assert.equal(admission.recordedSessionAdmissionPhase("error", ids, {
...cameras,
"camera.right": camera("ready"),
}), "error");
assert.equal(rerunPresentationStatus("ready", "error", true), "error");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, "error"),
"error",
);
});
test("camera admission enforces independent 16/128/512 MiB limits before fetching", () => {
const {
MAX_RECORDED_CAMERA_SOURCES,
MAX_RECORDED_MEDIA_SOURCE_BYTES,
MAX_RECORDED_SESSION_CAMERA_BYTES,
recordedCameraDescriptorPreflight,
} = admission;
assert.equal(MAX_RECORDED_CAMERA_SOURCES, 16);
assert.equal(MAX_RECORDED_MEDIA_SOURCE_BYTES, 128 * 1024 * 1024);
assert.equal(MAX_RECORDED_SESSION_CAMERA_BYTES, 512 * 1024 * 1024);
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 4 }, (_, index) => ({
id: `camera.${index}`,
byteLength: MAX_RECORDED_MEDIA_SOURCE_BYTES,
})),
), "ready");
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 17 }, (_, index) => ({ id: `camera.${index}`, byteLength: 1 })),
), "error");
assert.equal(recordedCameraDescriptorPreflight([
{ id: "camera.large", byteLength: MAX_RECORDED_MEDIA_SOURCE_BYTES + 1 },
]), "error");
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 5 }, (_, index) => ({
id: `camera.${index}`,
byteLength: 110 * 1024 * 1024,
})),
), "error");
assert.equal(recordedCameraDescriptorPreflight([
{ id: "camera.duplicate", byteLength: 1 },
{ id: "camera.duplicate", byteLength: 1 },
]), "error");
});
test("camera preparation remains single-flight and retains the loading permit", () => {
const ids = ["camera.first", "camera.preferred"];
const cameras = {
"camera.first": camera("loading"),
"camera.preferred": camera("pending"),
};
assert.deepEqual(
admission.nextRecordedCameraPreparationIds(
ids,
cameras,
new Set(["camera.preferred"]),
),
["camera.first"],
);
assert.deepEqual(
admission.nextRecordedCameraPreparationIds(ids, {
...cameras,
"camera.first": camera("ready"),
}),
["camera.preferred"],
);
});
test("new render workers close readiness and stale callbacks cannot reopen it", () => {
const ready = { ...camera("ready"), workerGeneration: 1 };
const remounted = admission.mergeRecordedCameraAdmission(ready, {
...camera("loading"),
workerGeneration: 2,
});
assert.equal(remounted.phase, "loading");
assert.equal(remounted.workerGeneration, 2);
assert.equal(admission.mergeRecordedCameraAdmission(remounted, {
...camera("ready"),
workerGeneration: 1,
}), remounted);
const admitted = admission.mergeRecordedCameraAdmission(remounted, {
...camera("ready"),
workerGeneration: 2,
});
assert.equal(admitted.phase, "ready");
const failed = admission.mergeRecordedCameraAdmission(admitted, {
...camera("error"),
workerGeneration: 2,
});
assert.equal(failed.phase, "error");
assert.equal(admission.mergeRecordedCameraAdmission(failed, {
...camera("loading"),
workerGeneration: 3,
}), failed);
});
test("recorded player is not mounted for a camera without a scheduler permit", async () => {
const source = await readFile(
new URL("../src/components/ObservationSources.tsx", import.meta.url),
"utf8",
);
const placeholder = source.indexOf("if (!prepareRecorded)");
const player = source.indexOf("<RecordedFmp4Player", placeholder);
assert.ok(placeholder >= 0 && player > placeholder);
assert.match(source.slice(placeholder, player), /return\s*\(/);
});
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let createRecordedOpenWatchdog;
let recordedOpenWatchdogTimeoutMs;
let rerunViewerInitialSource;
let resolveRecordedViewerSourceUrl;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
createRecordedOpenWatchdog,
recordedOpenWatchdogTimeoutMs,
rerunViewerInitialSource,
resolveRecordedViewerSourceUrl,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
after(async () => {
await server?.close();
});
const sourceUrl = "/api/v1/observation-sessions/session-atomic/recording.rrd";
const sha256 = "a".repeat(64);
const viewerSourceUrl = `${sourceUrl}?generation=${sha256}`;
test("only the canonical digest-bound generation URL reaches the native Rerun receiver", () => {
const descriptor = {
sourceUrl,
viewerSourceUrl,
byteLength: 246_331_680,
sha256,
};
const resolved = resolveRecordedViewerSourceUrl(descriptor, "http://mission-core.test");
assert.equal(resolved, `http://mission-core.test${viewerSourceUrl}`);
assert.equal(rerunViewerInitialSource(resolved), resolved);
for (const unsafeViewerSourceUrl of [
sourceUrl,
`${sourceUrl}?generation=${"b".repeat(64)}`,
`${viewerSourceUrl}&extra=true`,
`https://foreign.test${viewerSourceUrl}`,
]) {
assert.throws(
() => resolveRecordedViewerSourceUrl({
...descriptor,
viewerSourceUrl: unsafeViewerSourceUrl,
}, "http://mission-core.test"),
/Unsafe recorded RRD viewer descriptor/,
);
}
});
test("recorded admission watchdog is size-aware and exits an incomplete load", () => {
const smallDelay = recordedOpenWatchdogTimeoutMs(4);
const currentDelay = recordedOpenWatchdogTimeoutMs(246_331_680);
const largeDelay = recordedOpenWatchdogTimeoutMs(805_687_122);
assert.ok(smallDelay >= 120_000);
assert.ok(currentDelay >= smallDelay);
assert.ok(largeDelay > currentDelay);
assert.ok(largeDelay <= 1_800_000);
assert.ok(largeDelay > 12_000);
let scheduledCallback;
let scheduledDelay;
let timeoutCount = 0;
const cancelled = [];
const watchdog = createRecordedOpenWatchdog({
byteLength: 246_331_680,
schedule(callback, timeoutMs) {
scheduledCallback = callback;
scheduledDelay = timeoutMs;
return 17;
},
cancel(handle) {
cancelled.push(handle);
},
onTimeout() {
timeoutCount += 1;
},
});
watchdog.arm();
assert.equal(watchdog.pending(), true);
assert.equal(scheduledDelay, currentDelay);
scheduledCallback();
assert.equal(timeoutCount, 1);
assert.equal(watchdog.pending(), false);
assert.deepEqual(cancelled, []);
});
test("complete recorded admission clears its watchdog", () => {
let timeoutCount = 0;
const cancelled = [];
const watchdog = createRecordedOpenWatchdog({
byteLength: 805_687_122,
schedule() {
return 23;
},
cancel(handle) {
cancelled.push(handle);
},
onTimeout() {
timeoutCount += 1;
},
});
watchdog.arm();
watchdog.clear();
assert.equal(watchdog.pending(), false);
assert.equal(timeoutCount, 0);
assert.deepEqual(cancelled, [23]);
});
test("recorded RRD bytes are never split across LogChannel.send_rrd calls", async () => {
const source = await readFile(
new URL("../src/components/RerunViewport.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(source, /streamVerifiedRecordedRrd/);
assert.doesNotMatch(source, /missioncore\/recorded-recording/);
assert.doesNotMatch(source, /recordedChannel/);
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
assert.match(
source,
/recordingOpened = true;[\s\S]*if \(!isRecordedSource\) clearLiveRecordingOpenTimer\(\);/,
);
assert.match(
source,
/viewerStartResolved = true;\s*if \(isRecordedSource && !recordedSceneAdmitted\) recordedOpenWatchdog\?\.arm\(\);/,
);
assert.match(
source,
/if \(readyToRender && !readyPublished\)[\s\S]*clearRecordedAdmissionWatchdog\(\);/,
);
});
test("the complete vendor canvas host is hidden during partial and failed admission", async () => {
const css = await readFile(new URL("../src/styles/spatial.css", import.meta.url), "utf8");
assert.match(
css,
/\.rerun-viewport:is\(\[data-status="loading"\], \[data-status="error"\]\)\s+\.rerun-viewport__canvas\s*\{[^}]*visibility:\s*hidden;[^}]*pointer-events:\s*none;/s,
);
assert.doesNotMatch(
css,
/data-status="loading"[^}]*\.rerun-viewport__canvas\s*>\s*div/s,
);
});
@@ -0,0 +1,96 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let attemptRecordedAutoplay;
let createLatestAnimationFrameEmitter;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ attemptRecordedAutoplay, createLatestAnimationFrameEmitter } =
await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
after(async () => {
await server?.close();
});
test("recorded autoplay reports success only after seek and play both succeed", () => {
let seekAttempts = 0;
let playAttempts = 0;
const seekToStart = () => {
seekAttempts += 1;
};
const startPlaying = () => {
playAttempts += 1;
if (playAttempts === 1) throw new Error("receiver is not ready yet");
};
assert.equal(attemptRecordedAutoplay(seekToStart, startPlaying), false);
assert.equal(attemptRecordedAutoplay(seekToStart, startPlaying), true);
assert.equal(seekAttempts, 2);
assert.equal(playAttempts, 2);
});
test("recorded autoplay does not try play when the seek itself fails", () => {
let playAttempts = 0;
assert.equal(attemptRecordedAutoplay(
() => {
throw new Error("timeline is not ready yet");
},
() => {
playAttempts += 1;
},
), false);
assert.equal(playAttempts, 0);
});
test("time updates coalesce to the latest value once per animation frame", () => {
let nextHandle = 1;
const frames = new Map();
const cancelled = [];
const emitted = [];
const emitter = createLatestAnimationFrameEmitter({
emit(value) {
emitted.push(value);
},
requestFrame(callback) {
const handle = nextHandle;
nextHandle += 1;
frames.set(handle, callback);
return handle;
},
cancelFrame(handle) {
cancelled.push(handle);
frames.delete(handle);
},
});
emitter.push(1);
emitter.push(2);
emitter.push(3);
assert.equal(frames.size, 1);
assert.deepEqual(emitted, []);
const firstFrame = frames.get(1);
frames.delete(1);
firstFrame(0);
assert.deepEqual(emitted, [3]);
emitter.push(4);
assert.equal(frames.size, 1);
emitter.cancel();
assert.deepEqual(cancelled, [2]);
assert.deepEqual(emitted, [3]);
emitter.push(5);
assert.equal(frames.size, 0);
assert.deepEqual(emitted, [3]);
});
@@ -0,0 +1,170 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let canPublishRecordedPlaybackController;
let createRecordedAutoplayGate;
let isRecordedPlaybackReady;
let isRecordedPlaybackPresentationReady;
let isUsableRecordedPlaybackRange;
let recordedPlaybackBufferState;
let recordedPlaybackRangeWhenReady;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
isRecordedPlaybackReady,
isRecordedPlaybackPresentationReady,
isUsableRecordedPlaybackRange,
recordedPlaybackBufferState,
recordedPlaybackRangeWhenReady,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
after(async () => {
await server?.close();
});
test("a first frame reports buffer telemetry but is not ready for presentation", () => {
assert.equal(isUsableRecordedPlaybackRange(null), false);
assert.equal(isUsableRecordedPlaybackRange({ min: Number.NaN, max: 0 }), false);
assert.equal(isUsableRecordedPlaybackRange({ min: 2, max: 1 }), false);
assert.equal(isUsableRecordedPlaybackRange({ min: 0, max: 0 }), true);
const firstFrame = recordedPlaybackBufferState({ min: 0, max: 0 }, 20);
assert.deepEqual(firstFrame, {
bufferedEndNs: 0,
expectedEndNs: 20_000_000_000,
bufferProgress: 0,
fullyBuffered: false,
});
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), false);
assert.equal(recordedPlaybackRangeWhenReady({ min: 0, max: 0 }, firstFrame, true), null);
});
test("host timeline remains unmounted until the verified recording is fully ready", () => {
const partial = {
recordingId: "partial",
timeline: "session_time",
rangeNs: null,
currentNs: 0,
playing: false,
bufferedEndNs: 5,
expectedEndNs: 10,
bufferProgress: 0.5,
fullyBuffered: false,
};
assert.equal(isRecordedPlaybackPresentationReady("loading", partial), false);
assert.equal(isRecordedPlaybackPresentationReady("ready", partial), false);
assert.equal(isRecordedPlaybackPresentationReady("error", partial), false);
assert.equal(isRecordedPlaybackPresentationReady("ready", {
...partial,
rangeNs: { min: 0, max: 10 },
bufferedEndNs: 10,
bufferProgress: 1,
fullyBuffered: true,
}), true);
});
test("buffer progress grows independently and preserves the full-buffer tolerance", () => {
assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), {
bufferedEndNs: 5_000_000_000,
expectedEndNs: 20_000_000_000,
bufferProgress: 0.25,
fullyBuffered: false,
});
const complete = recordedPlaybackBufferState({ min: 0, max: 19_999_500_000 }, 20);
assert.equal(complete.bufferProgress, 0.999975);
assert.equal(complete.fullyBuffered, true);
assert.equal(isRecordedPlaybackReady(false, true, complete), false);
assert.equal(isRecordedPlaybackReady(true, false, complete), false);
assert.equal(isRecordedPlaybackReady(true, true, complete), true);
assert.equal(
recordedPlaybackRangeWhenReady({ min: 0, max: 19_999_500_000 }, complete, false),
null,
);
assert.deepEqual(
recordedPlaybackRangeWhenReady({ min: 0, max: 19_999_500_000 }, complete, true),
{ min: 0, max: 19_999_500_000 },
);
const wrongDeclaredStart = recordedPlaybackBufferState(
{ min: 5_000_000_000, max: 20_000_000_000 },
20,
0,
);
assert.equal(wrongDeclaredStart.bufferProgress, 1);
assert.equal(wrongDeclaredStart.fullyBuffered, false);
assert.equal(isRecordedPlaybackReady(true, true, wrongDeclaredStart), false);
});
test("recorded autoplay waits for the full range and then runs exactly once", () => {
const gate = createRecordedAutoplayGate();
const seeks = [];
let plays = 0;
const seek = (value) => seeks.push(value);
const play = () => {
plays += 1;
};
assert.equal(gate.attempt(false, true, true, { min: 0, max: 0 }, seek, play), false);
assert.equal(gate.attempt(true, false, true, { min: 0, max: 500_000_000 }, seek, play), false);
assert.equal(gate.attempt(true, true, false, { min: 0, max: 20_000_000_000 }, seek, play), false);
assert.deepEqual(seeks, []);
assert.equal(plays, 0);
assert.equal(gate.attempt(true, true, true, { min: 0, max: 20_000_000_000 }, seek, play), true);
assert.equal(gate.attempt(true, true, true, { min: 0, max: 20_000_000_000 }, seek, play), false);
assert.deepEqual(seeks, [0]);
assert.equal(plays, 1);
assert.equal(gate.attempted(), true);
});
test("a failed vendor autoplay attempt is consumed instead of rewinding later", () => {
const gate = createRecordedAutoplayGate();
let seeks = 0;
let plays = 0;
assert.equal(gate.attempt(
true,
true,
true,
{ min: 0, max: 1 },
() => {
seeks += 1;
},
() => {
plays += 1;
throw new Error("receiver raced its first frame");
},
), false);
assert.equal(gate.attempt(
true,
true,
true,
{ min: 0, max: 2 },
() => {
seeks += 1;
},
() => {
plays += 1;
},
), false);
assert.equal(seeks, 1);
assert.equal(plays, 1);
});
test("recorded playback controller stays unpublished until the aggregate gate is ready", () => {
assert.equal(canPublishRecordedPlaybackController(false, "ready"), false);
assert.equal(canPublishRecordedPlaybackController(true, "loading"), false);
assert.equal(canPublishRecordedPlaybackController(true, "error"), false);
assert.equal(canPublishRecordedPlaybackController(true, "ready"), true);
});
@@ -0,0 +1,178 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let createLatestAsyncCommitter;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ createLatestAsyncCommitter } = await server.ssrLoadModule(
"/src/core/runtime/latestAsyncCommitter.ts",
));
});
after(async () => {
await server?.close();
});
async function eventually(predicate, timeoutMs = 1_000) {
const deadline = Date.now() + timeoutMs;
while (!predicate()) {
if (Date.now() >= deadline) throw new Error("Timed out waiting for async commit queue");
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
test("viewer settings commits are serialized and intermediate slider values collapse", async () => {
const calls = [];
const resolvers = [];
const settled = [];
const committer = createLatestAsyncCommitter({
commit(value) {
calls.push(value);
return new Promise((resolve) => resolvers.push(resolve));
},
onSettled(result) {
settled.push(result);
},
});
committer.enqueue({ accumulationSeconds: 1 });
committer.enqueue({ accumulationSeconds: 2 });
committer.enqueue({ accumulationSeconds: 12 });
assert.deepEqual(calls, [{ accumulationSeconds: 1 }]);
assert.equal(committer.isBusy(), true);
resolvers.shift()(true);
await eventually(() => calls.length === 2);
assert.deepEqual(calls[1], { accumulationSeconds: 12 });
assert.equal(settled[0].superseded, true);
resolvers.shift()(true);
await eventually(() => !committer.isBusy());
assert.deepEqual(settled.map(({ value, applied, superseded }) => ({
value,
applied,
superseded,
})), [
{ value: { accumulationSeconds: 1 }, applied: true, superseded: true },
{ value: { accumulationSeconds: 12 }, applied: true, superseded: false },
]);
});
test("viewer settings commit failures settle as rejected without wedging the queue", async () => {
const settled = [];
const committer = createLatestAsyncCommitter({
async commit(value) {
if (value === "broken") throw new Error("offline");
return true;
},
onSettled(result) {
settled.push(result);
},
});
committer.enqueue("broken");
await eventually(() => settled.length === 1);
committer.enqueue("healthy");
await eventually(() => settled.length === 2);
assert.deepEqual(settled.map(({ value, applied }) => ({ value, applied })), [
{ value: "broken", applied: false },
{ value: "healthy", applied: true },
]);
assert.equal(committer.isBusy(), false);
});
test("waitForIdle resolves only after the running commit and latest queued value settle", async () => {
const calls = [];
const resolvers = [];
const committer = createLatestAsyncCommitter({
commit(value) {
calls.push(value);
return new Promise((resolve) => resolvers.push(resolve));
},
});
committer.enqueue("initial");
committer.enqueue("latest");
let idle = false;
const idlePromise = committer.waitForIdle().then(() => {
idle = true;
});
await Promise.resolve();
assert.equal(idle, false);
resolvers.shift()(true);
await eventually(() => calls.length === 2);
assert.deepEqual(calls, ["initial", "latest"]);
assert.equal(idle, false);
resolvers.shift()(true);
await idlePromise;
assert.equal(idle, true);
assert.equal(committer.isBusy(), false);
});
test("waitForIdle observes work enqueued by onSettled and resolves immediately when idle", async () => {
const calls = [];
let committer;
committer = createLatestAsyncCommitter({
async commit(value) {
calls.push(value);
return true;
},
onSettled({ value }) {
if (value === "first") committer.enqueue("follow-up");
},
});
await committer.waitForIdle();
committer.enqueue("first");
await committer.waitForIdle();
assert.deepEqual(calls, ["first", "follow-up"]);
assert.equal(committer.isBusy(), false);
});
test("layout serialization can flush a staged setting and read the confirmed latest value", async () => {
let staged = { pointSize: 2 };
let confirmed = { pointSize: 1 };
let resolveCommit;
const committer = createLatestAsyncCommitter({
commit() {
return new Promise((resolve) => {
resolveCommit = resolve;
});
},
onSettled({ value, applied }) {
if (applied) confirmed = value;
},
});
const saveLayout = async () => {
committer.enqueue(staged);
await committer.waitForIdle();
return { sceneSettings: confirmed };
};
const savePromise = saveLayout();
staged = { pointSize: 9 };
let saved = false;
void savePromise.then(() => {
saved = true;
});
await Promise.resolve();
assert.equal(saved, false);
resolveCommit(true);
const layout = await savePromise;
assert.deepEqual(layout, { sceneSettings: { pointSize: 2 } });
});
@@ -0,0 +1,229 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decodeObservationWorkspaceLayoutProfile;
let encodeObservationWorkspaceLayoutProfile;
let fetchObservationWorkspaceLayoutProfile;
let normalizeObservationWindowRect;
let projectObservationLayoutSnapshot;
let saveObservationWorkspaceLayoutProfile;
let WorkspaceLayoutApiError;
let WorkspaceLayoutContractError;
let observationPresentationSourceAfterLayoutApply;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
decodeObservationWorkspaceLayoutProfile,
encodeObservationWorkspaceLayoutProfile,
fetchObservationWorkspaceLayoutProfile,
normalizeObservationWindowRect,
projectObservationLayoutSnapshot,
saveObservationWorkspaceLayoutProfile,
WorkspaceLayoutApiError,
WorkspaceLayoutContractError,
} = await server.ssrLoadModule("/src/core/observation/workspaceLayout.ts"));
({ observationPresentationSourceAfterLayoutApply } = await server.ssrLoadModule(
"/src/core/observation/useObservationLayout.ts",
));
});
after(async () => {
await server?.close();
});
function wireProfile(overrides = {}) {
return {
version: 1,
revision: 7,
workspace_id: "observation.spatial",
scene_settings: {
projection: "3d",
point_size: 2.5,
color_mode: "intensity",
palette: "turbo",
custom_color: "#35d7c1",
accumulation_seconds: 12,
show_points: true,
show_trajectory: true,
show_grid: true,
show_labels: false,
show_camera_frustums: true,
},
tool_windows: {
sources_open: true,
display_open: false,
layers_open: true,
order: ["sources", "layers", "display"],
},
visible_source_ids: ["spatial.point-cloud.live", "camera.left"],
active_floating_source_id: "camera.left",
window_rects: {
"camera.left": { x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
"camera.right": { x: 0.55, y: 0.1, width: 0.4, height: 0.4 },
},
viewport_size: { width: 1_000, height: 500 },
...overrides,
};
}
test("workspace layout performs a lossless strict wire/camel/wire round trip", () => {
const wire = wireProfile();
const profile = decodeObservationWorkspaceLayoutProfile(wire);
assert.deepEqual(profile.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);
assert.deepEqual(profile.toolWindows.order, ["sources", "layers", "display"]);
assert.equal(profile.sceneSettings.pointSize, 2.5);
assert.deepEqual(encodeObservationWorkspaceLayoutProfile(profile), wire);
});
test("workspace layout rejects unknown fields, transient transport data and invalid schema values", () => {
assert.throws(
() => decodeObservationWorkspaceLayoutProfile({
...wireProfile(),
source_url: "ws://192.168.68.52:9877",
}),
WorkspaceLayoutContractError,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({ version: 2 })),
/Неподдерживаемая версия/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({ revision: 1.5 })),
/revision/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
visible_source_ids: ["../camera"],
active_floating_source_id: null,
})),
/стабильный идентификатор/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
window_rects: { "camera.left": { x: 0.8, y: 0, width: 0.4, height: 0.5 } },
})),
/выходит за нормализованные границы/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
tool_windows: {
sources_open: true,
display_open: true,
layers_open: true,
order: ["sources", "sources", "layers"],
},
})),
/перестановкой/,
);
assert.throws(
() => decodeObservationWorkspaceLayoutProfile(wireProfile({
scene_settings: { ...wireProfile().scene_settings, point_size: Number.POSITIVE_INFINITY },
})),
/point_size/,
);
});
test("restored desired layout survives an empty catalog and reveals only known stable sources later", () => {
const profile = decodeObservationWorkspaceLayoutProfile(wireProfile());
const snapshot = {
visibleSourceIds: profile.visibleSourceIds,
activeFloatingSourceId: profile.activeFloatingSourceId,
windowRects: profile.windowRects,
viewportSize: profile.viewportSize,
};
const empty = projectObservationLayoutSnapshot(snapshot, new Set(), { width: 500, height: 1_000 });
assert.deepEqual(empty, {
visibleSourceIds: [],
activeFloatingSourceId: null,
windowRects: {},
});
assert.deepEqual(snapshot.visibleSourceIds, ["spatial.point-cloud.live", "camera.left"]);
const cameraAppeared = projectObservationLayoutSnapshot(
snapshot,
new Set(["camera.left"]),
{ width: 500, height: 1_000 },
);
assert.deepEqual(cameraAppeared.visibleSourceIds, ["camera.left"]);
assert.equal(cameraAppeared.activeFloatingSourceId, "camera.left");
assert.deepEqual(cameraAppeared.windowRects["camera.left"], {
x: 50,
y: 100,
width: 200,
height: 400,
});
});
test("window rectangles normalize once and project proportionally into a different viewport", () => {
assert.deepEqual(
normalizeObservationWindowRect(
{ x: 100, y: 50, width: 400, height: 200 },
{ width: 1_000, height: 500 },
),
{ x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
);
});
test("fullscreen presentation survives the viewport resize it causes", () => {
assert.equal(
observationPresentationSourceAfterLayoutApply("camera.left", "preserve"),
"camera.left",
);
assert.equal(
observationPresentationSourceAfterLayoutApply("camera.left", "reset"),
null,
);
});
test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
const calls = [];
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
const saved = await saveObservationWorkspaceLayoutProfile(current, {
fetcher: async (input, init) => {
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
return new Response(JSON.stringify(wireProfile({ revision: 8 })), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(calls.length, 1);
assert.equal(calls[0].input, "/api/v1/workspace-layouts/observation.spatial");
assert.equal(calls[0].init.method, "PUT");
assert.equal(calls[0].init.headers["If-Match"], '"7"');
assert.equal(calls[0].body.revision, 7);
assert.equal(calls[0].body.workspace_id, "observation.spatial");
assert.equal(saved.revision, 8);
});
test("workspace layout API treats 404 as no profile and exposes revision conflicts", async () => {
assert.equal(
await fetchObservationWorkspaceLayoutProfile({
fetcher: async () => new Response(null, { status: 404 }),
}),
null,
);
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());
await assert.rejects(
saveObservationWorkspaceLayoutProfile(current, {
fetcher: async () => new Response(JSON.stringify({ detail: "revision mismatch" }), {
status: 412,
headers: { "Content-Type": "application/json" },
}),
}),
(error) => error instanceof WorkspaceLayoutApiError &&
error.conflict && error.status === 412 && error.message === "revision mismatch",
);
});