Files
NODEDC_MISSION_CORE/apps/control-station/test/liveViewerDiagnostics.test.mjs
T
DCCONSTRUCTIONS 0ca7316a24 wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
2026-08-14 14:57:50 +03:00

217 lines
6.9 KiB
JavaScript

import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let createLiveViewerDiagnosticLifecycle;
let createAbortFencedBuildVerifier;
let createUiBuildStaleCoordinator;
let liveViewerDiagnosticBody;
let server;
let uiBuildIdFromModuleScripts;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
createAbortFencedBuildVerifier,
createLiveViewerDiagnosticLifecycle,
createUiBuildStaleCoordinator,
liveViewerDiagnosticBody,
uiBuildIdFromModuleScripts,
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
});
after(async () => {
await server?.close();
});
function createFakeScheduler() {
let now = 0;
let nextHandle = 1;
const jobs = new Map();
const schedule = (callback, delay, interval) => {
const handle = nextHandle;
nextHandle += 1;
jobs.set(handle, { callback, due: now + delay, interval });
return handle;
};
const clear = (handle) => jobs.delete(handle);
return {
scheduler: {
setTimeout: (callback, delay) => schedule(callback, delay, null),
clearTimeout: clear,
setInterval: (callback, delay) => schedule(callback, delay, delay),
clearInterval: clear,
},
advance(milliseconds) {
const target = now + milliseconds;
while (true) {
const next = [...jobs.entries()]
.filter(([, job]) => job.due <= target)
.sort((left, right) => left[1].due - right[1].due)[0];
if (!next) break;
const [handle, job] = next;
now = job.due;
if (job.interval === null) jobs.delete(handle);
else job.due += job.interval;
job.callback();
}
now = target;
},
pending: () => jobs.size,
};
}
const lineage = (viewerInstanceId, lifecycleGeneration = 1) => ({
uiBuildId: "/assets/index-abcdefgh.js",
documentInstanceId: "00000000-0000-4000-8000-000000000001",
viewerInstanceId,
lifecycleGeneration,
});
test("mounted viewer admission terminally fences 60 seconds of stale timers", () => {
const clock = createFakeScheduler();
const callbacks = [];
const posts = [];
const lifecycle = createLiveViewerDiagnosticLifecycle({
lineage: lineage("00000000-0000-4000-8000-000000000011"),
scheduler: clock.scheduler,
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
buildVerifier: () => undefined,
});
lifecycle.armAdmissionTimeout(() => callbacks.push("timeout"), 12_000);
lifecycle.armAdmissionInterval(() => callbacks.push("discovery"), 100);
lifecycle.markAdmitted();
lifecycle.post({ eventCode: "live_receiver_active_store_admitted" });
clock.advance(60_000);
assert.deepEqual(callbacks, []);
assert.equal(clock.pending(), 0);
assert.equal(posts.length, 1);
assert.equal(posts[0].eventLineage.lifecycleGeneration, 1);
});
test("two mounted viewers keep timer and diagnostic lineage isolated", () => {
const clock = createFakeScheduler();
const posts = [];
const first = createLiveViewerDiagnosticLifecycle({
lineage: lineage("00000000-0000-4000-8000-000000000021"),
scheduler: clock.scheduler,
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
buildVerifier: () => undefined,
});
const second = createLiveViewerDiagnosticLifecycle({
lineage: lineage("00000000-0000-4000-8000-000000000022", 7),
scheduler: clock.scheduler,
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
buildVerifier: () => undefined,
});
first.armAdmissionTimeout(() => {
first.post({ eventCode: "live_receiver_error" });
}, 12_000);
second.armAdmissionTimeout(() => {
second.post({ eventCode: "live_receiver_error" });
}, 12_000);
first.markAdmitted();
clock.advance(12_000);
assert.equal(posts.length, 1);
assert.equal(
posts[0].eventLineage.viewerInstanceId,
"00000000-0000-4000-8000-000000000022",
);
assert.equal(posts[0].eventLineage.lifecycleGeneration, 7);
});
test("stale-build and unmount fence callbacks before one reload", () => {
const clock = createFakeScheduler();
const order = [];
const posts = [];
const lifecycle = createLiveViewerDiagnosticLifecycle({
lineage: lineage("00000000-0000-4000-8000-000000000031"),
scheduler: clock.scheduler,
diagnosticPoster: (event) => posts.push(event),
buildVerifier: () => undefined,
});
lifecycle.armAdmissionTimeout(() => {
lifecycle.post({ eventCode: "live_receiver_error" });
}, 12_000);
const coordinator = createUiBuildStaleCoordinator({
scheduleReload: (callback, delay) => {
order.push(`scheduled:${delay}`);
clock.scheduler.setTimeout(callback, delay);
},
reload: () => order.push("reload"),
});
coordinator.subscribe(() => {
order.push("local-transports-closed");
lifecycle.dispose();
});
coordinator.report({
loadedUiBuildId: "/assets/index-abcdefgh.js",
expectedUiBuildId: "/assets/index-ijklmnop.js",
});
coordinator.report({
loadedUiBuildId: "/assets/index-abcdefgh.js",
expectedUiBuildId: "/assets/index-qrstuvwx.js",
});
clock.advance(60_000);
lifecycle.post({ eventCode: "live_receiver_error" });
assert.deepEqual(order, ["local-transports-closed", "scheduled:50", "reload"]);
assert.deepEqual(posts, []);
assert.equal(lifecycle.active(), false);
});
test("last unsubscribe fences an already queued build verification callback", () => {
const controller = new AbortController();
const observedSignals = [];
const queuedVerify = createAbortFencedBuildVerifier(
controller.signal,
(signal) => observedSignals.push(signal),
);
queuedVerify();
// stopBuildMonitor aborts the locally captured controller when the last
// mounted viewer unsubscribes. A browser callback already queued before the
// interval/listener removal can still run once, but cannot start a fetch.
controller.abort();
queuedVerify();
assert.deepEqual(observedSignals, [controller.signal]);
assert.equal(observedSignals[0].aborted, true);
});
test("diagnostic body and build id retain exact document/viewer/build lineage", () => {
const eventLineage = lineage("00000000-0000-4000-8000-000000000041", 9);
assert.deepEqual(
liveViewerDiagnosticBody(
{ eventCode: "live_receiver_recovered", streamId: "acquisition-42" },
eventLineage,
),
{
schema_version: "missioncore.live-viewer-diagnostic/v2",
event_code: "live_receiver_recovered",
ui_build_id: "/assets/index-abcdefgh.js",
document_instance_id: "00000000-0000-4000-8000-000000000001",
viewer_instance_id: "00000000-0000-4000-8000-000000000041",
lifecycle_generation: 9,
stream_id: "acquisition-42",
},
);
assert.equal(
uiBuildIdFromModuleScripts(
["https://mission.local/assets/index-dT7dN-y4.js"],
"https://mission.local/park",
),
"/assets/index-dT7dN-y4.js",
);
});