fix(k1): harden live handoff and camera recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 13:09:24 +03:00
parent eaad9deda1
commit 85035fa07b
26 changed files with 1478 additions and 170 deletions
@@ -20,6 +20,7 @@ let operatorIntentGeneration;
let configuration;
let compatibility;
let networkProvisionFailureMessage;
let awaitNetworkProvisionSettlementAfterLostResponse;
let discoveryScanFailureMessage;
let connectionVerificationFailureMessage;
let operationById;
@@ -99,6 +100,7 @@ before(async () => {
));
({
networkProvisionFailureMessage,
awaitNetworkProvisionSettlementAfterLostResponse,
discoveryScanFailureMessage,
connectionVerificationFailureMessage,
operationById,
@@ -1043,6 +1045,56 @@ test("K1 API uses bounded no-retry timeout classes per operation family", async
assert.doesNotMatch(apiSource, /automaticRetry\s*=\s*true/);
});
test("lost Quick response follows the admitted Apply by journal reads only", async () => {
const idempotencyKey = "network-provision:quick-lost-response";
let nowMs = 1_000;
let reads = 0;
const accepted = [];
const operation = {
operation_id: "op-quick-lost-response",
action: "network.provision",
status: "running",
idempotency_key: idempotencyKey,
deadline_at: new Date(5_000).toISOString(),
};
const initialState = {
snapshot_runtime_id: "runtime-quick-lost-response",
operations: [operation],
};
const terminalState = {
...initialState,
operations: [{
...operation,
status: "succeeded",
result: { phase: "network_applied" },
}],
};
const settled = await awaitNetworkProvisionSettlementAfterLostResponse(
initialState,
idempotencyKey,
async () => {
reads += 1;
if (reads === 1) {
throw new ApiError("transient localhost handoff", 0, true);
}
return terminalState;
},
(state) => accepted.push(state),
() => {},
{
now: () => nowMs,
wait: async (delayMs) => {
nowMs += delayMs;
},
},
);
assert.equal(reads, 2);
assert.deepEqual(accepted, [terminalState]);
assert.equal(settled.operations[0].status, "succeeded");
});
test("one explicit K1 operator action supplies the backend physical acceptance", () => {
assert.deepEqual(physicalCommandConfirmation.operatorActionPhysicalAcceptance(), {
operator_present: true,
@@ -72,6 +72,7 @@ let transportRefEquivalenceKey;
let shouldRevealProvisioningNetworkStep;
let retiredPhysicalReopenAuthority;
let SnapshotRuntimeActionArbiter;
let runtimeActionResponseAlreadyAccepted;
let connectionActionAuthoritySnapshot;
let exactAppliedNetworkIntentCompleted;
let selectMonotonicXgridsState;
@@ -106,6 +107,7 @@ before(async () => {
SnapshotRuntimeActionArbiter,
connectionActionAuthoritySnapshot,
exactAppliedNetworkIntentCompleted,
runtimeActionResponseAlreadyAccepted,
} = await server.ssrLoadModule(
"@xgrids-k1/frontend/useXgridsK1Runtime.ts",
));
@@ -2026,6 +2028,27 @@ test("K1 runtime replacement retires A and isolates a new B action from late set
assert.equal(pendingAction, null);
});
test("successful action response may be superseded only by the same runtime revision", () => {
const response = {
snapshot_runtime_id: "runtime-a",
snapshot_revision: 41,
};
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
snapshot_runtime_id: "runtime-a",
snapshot_revision: 42,
}), true);
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
snapshot_runtime_id: "runtime-a",
snapshot_revision: 40,
}), false);
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
snapshot_runtime_id: "runtime-b",
snapshot_revision: 99,
}), false);
assert.equal(runtimeActionResponseAlreadyAccepted(response, null), false);
});
test("explicit scenario reset B supersedes pending callback A in one runtime", () => {
const arbiter = new SnapshotRuntimeActionArbiter();
const actionA = arbiter.begin(12);
@@ -273,6 +273,21 @@ test("opening receiver preserves partial store replay while backend publication
}
});
test("new receiver grants its first confirmed backend publication a full open window", () => {
const openState = initialLiveReceiverOpenWatchdogState(null, 0);
const recoveryState = initialLiveReceiverRecoveryState();
const firstObservedPublication = advanceLiveReceiverOpenWatchdog(
openState,
recoveryState,
1,
4_000,
);
assert.equal(firstObservedPublication.signal, "wait-for-store");
assert.equal(firstObservedPublication.state.lastBackendActivitySequence, 1);
assert.deepEqual(firstObservedPublication.recoveryState, recoveryState);
});
test("unchanged opening sequence delegates to bounded receiver restart", () => {
const openState = initialLiveReceiverOpenWatchdogState(486);
const recoveryState = initialLiveReceiverRecoveryState();
@@ -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,
},
);
});
@@ -16,6 +16,7 @@ let cameraTransportRecoveryIsCurrent;
let cameraTransportCanOpen;
let cameraPendingQueueCanAccept;
let cameraTransportCloseRecoveryMessage;
let cameraPlaybackRecoveryFailureStage;
let createCameraStartupWatchdog;
let cameraStartupWatchdogRecoveryMessage;
let CAMERA_FIRST_MEDIA_TIMEOUT_MS;
@@ -69,6 +70,7 @@ before(async () => {
cameraTransportCanOpen,
cameraPendingQueueCanAccept,
cameraTransportCloseRecoveryMessage,
cameraPlaybackRecoveryFailureStage,
createCameraStartupWatchdog,
cameraStartupWatchdogRecoveryMessage,
CAMERA_FIRST_MEDIA_TIMEOUT_MS,
@@ -1297,6 +1299,25 @@ test("server slow-reader close is an automatic browser-only camera recovery", ()
assert.match(cameraTransportCloseRecoveryMessage(1_011), /восстанавливаем/);
});
test("camera transport recovery events keep exact diagnostic causes", () => {
assert.equal(
cameraPlaybackRecoveryFailureStage({ type: "heartbeat" }),
"camera-heartbeat-reopen",
);
assert.equal(
cameraPlaybackRecoveryFailureStage({ type: "document-visible" }),
"camera-visibility-reopen",
);
assert.equal(
cameraPlaybackRecoveryFailureStage({ type: "network-online" }),
"camera-network-online-reopen",
);
assert.equal(
cameraPlaybackRecoveryFailureStage({ type: "page-restore", persisted: true }),
"camera-page-restore-reopen",
);
});
test("a laptop sleep gap reopens only the exact authoritative live camera transport", () => {
const source = xgridsK1ObservationSources(
cameraStreamingState("sensor.camera.right"),
@@ -237,6 +237,14 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
source,
/if \(readyToRender && !readyPublished\)[\s\S]*eventCode: "live_receiver_active_store_admitted"[\s\S]*diagnosticLifecycle\.markAdmitted\(\)/,
);
assert.match(
source,
/liveOpenWatchdog = initialLiveReceiverOpenWatchdogState\(null, Date\.now\(\)\)/,
);
assert.doesNotMatch(
source,
/initialLiveReceiverOpenWatchdogState\(\s*liveActivitySequenceRef\.current/,
);
assert.doesNotMatch(
source,
/\], \[followLive, liveRecoveryAuthorityIdentity, liveStreamId, sourceUrl\]\);/,
@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let viewerSettingsTargetIdentity;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ viewerSettingsTargetIdentity } = await server.ssrLoadModule(
"/src/core/observation/viewerSettingsTarget.ts",
));
});
after(async () => {
await server?.close();
});
function runtimeState({ acquisitionId, sessionId = "session-a", deviceId = "device-a" }) {
return {
phase: "connected",
sourceMode: "idle",
activeDevice: {
pluginId: "xgrids-k1",
modelId: "lixel-k1",
instanceId: "instance-a",
displayName: "K1",
},
deviceSession: { sessionId, deviceId },
acquisition: acquisitionId
? {
acquisitionId,
deviceId,
deviceSessionId: sessionId,
compatibilityProfileId: "profile-a",
controlMode: "live",
state: "streaming",
stateRevision: 1,
operatorInstructions: [],
}
: null,
};
}
test("new acquisition on the same control session keeps the viewer settings target stable", () => {
const beforeStart = viewerSettingsTargetIdentity(runtimeState({}));
const firstRun = viewerSettingsTargetIdentity(runtimeState({ acquisitionId: "acquisition-a" }));
const secondRun = viewerSettingsTargetIdentity(runtimeState({ acquisitionId: "acquisition-b" }));
assert.equal(firstRun, beforeStart);
assert.equal(secondRun, beforeStart);
});
test("temporary loss of control authority does not reapply settings within one device session", () => {
const authoritative = runtimeState({ acquisitionId: "acquisition-a" });
const authorityUnavailable = {
...authoritative,
activeDevice: null,
phase: "starting",
};
assert.equal(
viewerSettingsTargetIdentity(authorityUnavailable),
viewerSettingsTargetIdentity(authoritative),
);
});
test("a new control session or device produces a new viewer settings target", () => {
const original = viewerSettingsTargetIdentity(runtimeState({ acquisitionId: "acquisition-a" }));
assert.notEqual(
viewerSettingsTargetIdentity(runtimeState({ sessionId: "session-b" })),
original,
);
assert.notEqual(
viewerSettingsTargetIdentity(runtimeState({ deviceId: "device-b" })),
original,
);
});
test("an unconfigured runtime uses the local target", () => {
assert.equal(viewerSettingsTargetIdentity(null), "local-runtime");
});