fix(k1): harden live handoff and camera recovery
This commit is contained in:
@@ -9,6 +9,7 @@ let createUiBuildStaleCoordinator;
|
||||
let liveViewerDiagnosticBody;
|
||||
let server;
|
||||
let uiBuildIdFromModuleScripts;
|
||||
let verifyLiveViewerClientBuild;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -22,6 +23,7 @@ before(async () => {
|
||||
createUiBuildStaleCoordinator,
|
||||
liveViewerDiagnosticBody,
|
||||
uiBuildIdFromModuleScripts,
|
||||
verifyLiveViewerClientBuild,
|
||||
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
|
||||
});
|
||||
|
||||
@@ -129,9 +131,9 @@ test("two mounted viewers keep timer and diagnostic lineage isolated", () => {
|
||||
assert.equal(posts[0].eventLineage.lifecycleGeneration, 7);
|
||||
});
|
||||
|
||||
test("stale-build and unmount fence callbacks before one reload", () => {
|
||||
test("stale build drift neither fences transports nor reloads the document", () => {
|
||||
const clock = createFakeScheduler();
|
||||
const order = [];
|
||||
const sideEffects = [];
|
||||
const posts = [];
|
||||
const lifecycle = createLiveViewerDiagnosticLifecycle({
|
||||
lineage: lineage("00000000-0000-4000-8000-000000000031"),
|
||||
@@ -142,15 +144,9 @@ test("stale-build and unmount fence callbacks before one reload", () => {
|
||||
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"),
|
||||
});
|
||||
const coordinator = createUiBuildStaleCoordinator();
|
||||
coordinator.subscribe(() => {
|
||||
order.push("local-transports-closed");
|
||||
sideEffects.push("local-transports-closed");
|
||||
lifecycle.dispose();
|
||||
});
|
||||
|
||||
@@ -165,9 +161,47 @@ test("stale-build and unmount fence callbacks before one reload", () => {
|
||||
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);
|
||||
assert.deepEqual(sideEffects, []);
|
||||
assert.deepEqual(posts, [
|
||||
{ eventCode: "live_receiver_error" },
|
||||
{ eventCode: "live_receiver_error" },
|
||||
]);
|
||||
assert.equal(lifecycle.active(), true);
|
||||
assert.equal(coordinator.stale(), true);
|
||||
});
|
||||
|
||||
test("build drift is reported once with the exact loaded build", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.live-viewer-client-contract/v1",
|
||||
status: "ready",
|
||||
ui_build_id: "/assets/index-ijklmnop.js",
|
||||
diagnostic_schema_version: "missioncore.live-viewer-diagnostic/v2",
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "X-MissionCore-UI-Build": "/assets/index-ijklmnop.js" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const eventLineage = lineage("00000000-0000-4000-8000-000000000032");
|
||||
verifyLiveViewerClientBuild(eventLineage);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
verifyLiveViewerClientBuild(eventLineage);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, "/api/v1/viewer/client-contract");
|
||||
assert.equal(
|
||||
calls[0].options.headers["X-MissionCore-UI-Build"],
|
||||
"/assets/index-abcdefgh.js",
|
||||
);
|
||||
});
|
||||
|
||||
test("last unsubscribe fences an already queued build verification callback", () => {
|
||||
@@ -213,4 +247,41 @@ test("diagnostic body and build id retain exact document/viewer/build lineage",
|
||||
),
|
||||
"/assets/index-dT7dN-y4.js",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
liveViewerDiagnosticBody(
|
||||
{
|
||||
eventCode: "live_camera_transport_restart_requested",
|
||||
failureStage: "camera-queue-capacity",
|
||||
streamId: "camera-preview-2",
|
||||
cameraQueueBytes: 12_000_000,
|
||||
cameraQueueSegments: 96,
|
||||
cameraRetryCount: 3,
|
||||
websocketCloseCode: 4_008,
|
||||
transportEpoch: 7,
|
||||
cameraAppendErrorName: "InvalidStateError",
|
||||
cameraMediaSourceState: "open",
|
||||
cameraVideoErrorCode: 3,
|
||||
},
|
||||
eventLineage,
|
||||
),
|
||||
{
|
||||
schema_version: "missioncore.live-viewer-diagnostic/v2",
|
||||
event_code: "live_camera_transport_restart_requested",
|
||||
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,
|
||||
failure_stage: "camera-queue-capacity",
|
||||
stream_id: "camera-preview-2",
|
||||
camera_queue_bytes: 12_000_000,
|
||||
camera_queue_segments: 96,
|
||||
camera_retry_count: 3,
|
||||
websocket_close_code: 4_008,
|
||||
transport_epoch: 7,
|
||||
camera_append_error_name: "InvalidStateError",
|
||||
camera_media_source_state: "open",
|
||||
camera_video_error_code: 3,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user