fix(k1): stabilize live recovery and media admission

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 00:03:01 +03:00
parent 7217244886
commit eaad9deda1
29 changed files with 1645 additions and 199 deletions
@@ -620,6 +620,53 @@ test("canonical K1 preparation stops before START and guards every async stage",
);
});
test("K1 control-phase polling fails closed instead of stacking timed-out state reads", async () => {
const hookSource = await readFile(
new URL(
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
import.meta.url,
),
"utf8",
);
const controlPollingLoop = hookSource.slice(
hookSource.indexOf("async function waitForControlPhase"),
hookSource.indexOf("async function waitForPhysicalReconciliationProof"),
);
assert.match(hookSource, /const CONTROL_PHASE_WAIT_TIMEOUT_MS = 120_000/);
assert.match(controlPollingLoop, /xgridsK1Api\.getState\(\)/);
assert.doesNotMatch(controlPollingLoop, /ApiRequestTimeoutError/);
assert.doesNotMatch(controlPollingLoop, /catch \(error\)/);
assert.doesNotMatch(
controlPollingLoop,
/(?:openApplicationControlSession|enterApplicationWorkspace|prepareAcquisition|startAcquisition|stopAcquisition)\(/,
);
});
test("K1 frontend coalesces concurrent read-only state requests without caching them", async () => {
const apiSource = await readFile(
new URL(
"../../../plugins/xgrids-k1/frontend/src/api.ts",
import.meta.url,
),
"utf8",
);
const singleFlight = apiSource.slice(
apiSource.indexOf("let stateReadInFlight"),
apiSource.indexOf("export const xgridsK1Api"),
);
const getState = apiSource.slice(
apiSource.indexOf("async getState"),
apiSource.indexOf("scanBle("),
);
assert.match(singleFlight, /if \(stateReadInFlight\) return stateReadInFlight/);
assert.match(singleFlight, /invokeState\(xgridsK1Actions\.stateRead\)/);
assert.match(singleFlight, /stateReadInFlight === request[\s\S]*?stateReadInFlight = null/);
assert.match(getState, /return readStateSingleFlight\(\)/);
assert.doesNotMatch(singleFlight, /setTimeout|setInterval|retry/i);
});
test("K1 control-session CAS is exact, integer-only and mapped for each mutation family", () => {
const state = {
application_control_session: {
@@ -244,13 +244,16 @@ test("connecting Rerun authority requires an exact recovery generation lease", (
);
});
test("opening receiver gets bounded rolling patience while backend publication advances", () => {
test("opening receiver preserves partial store replay while backend publication advances", () => {
let openState = initialLiveReceiverOpenWatchdogState(0, 0);
let recoveryState = initialLiveReceiverRecoveryState();
const samples = [
[134, 3_999, "wait-for-store"],
[266, 4_000, "refresh-receiver"],
[266, 4_000, "wait-for-store"],
[380, 8_000, "wait-for-store"],
[486, 12_000, "wait-for-store"],
[700, 120_000, "wait-for-store"],
];
for (const [backendActivitySequence, nowMs, expectedSignal] of samples) {
const observed = advanceLiveReceiverOpenWatchdog(
@@ -302,17 +305,17 @@ test("fresh backend progress preserves earlier restart debt until viewer admissi
});
});
test("aged active receiver refresh does not spend or erase restart debt", () => {
test("aged active receiver remains intact and preserves restart debt", () => {
const openState = initialLiveReceiverOpenWatchdogState(486, 0);
const consumedRestart = requestLiveReceiverRecovery(initialLiveReceiverRecoveryState());
const observed = advanceLiveReceiverOpenWatchdog(
openState,
consumedRestart.state,
900,
4_000,
12_000,
);
assert.equal(observed.signal, "refresh-receiver");
assert.equal(observed.signal, "wait-for-store");
assert.deepEqual(observed.recoveryState, consumedRestart.state);
assert.equal(observed.openForMs, 4_000);
assert.equal(observed.openForMs, 12_000);
});
@@ -1199,7 +1199,7 @@ test("camera startup watchdog replaces an open-but-silent MSE or WebSocket", ()
);
});
test("first media starts one non-sliding first-playable-frame deadline", () => {
test("continuing media extends the first-playable-frame deadline", () => {
const scheduled = new Map();
const cancelled = [];
const timeouts = [];
@@ -1228,8 +1228,14 @@ test("first media starts one non-sliding first-playable-frame deadline", () => {
assert.equal(scheduled.get(playableHandle).timeoutMs, CAMERA_FIRST_PLAYABLE_FRAME_TIMEOUT_MS);
watchdog.markMediaReceived();
assert.equal(nextHandle - 1, playableHandle, "later fragments must not extend the deadline");
scheduled.get(playableHandle).callback();
const extendedPlayableHandle = nextHandle - 1;
assert.notEqual(extendedPlayableHandle, playableHandle);
assert.deepEqual(cancelled, [10, playableHandle]);
assert.equal(
scheduled.get(extendedPlayableHandle).timeoutMs,
CAMERA_FIRST_PLAYABLE_FRAME_TIMEOUT_MS,
);
scheduled.get(extendedPlayableHandle).callback();
assert.deepEqual(timeouts, ["first-playable-frame"]);
assert.match(
cameraStartupWatchdogRecoveryMessage("first-playable-frame"),
@@ -1306,6 +1312,7 @@ test("a laptop sleep gap reopens only the exact authoritative live camera transp
now: 7_000,
documentVisible: true,
networkOnline: true,
transportHealthy: false,
});
assert.equal(wake.reopen, true);
assert.equal(wake.state.lastReopenAt, 7_000);
@@ -1369,15 +1376,16 @@ test("visibility, pageshow and online wake burst owns one replacement decoder",
assert.equal(secondWake.state.lastReopenAt, 18_000);
});
test("healthy visible camera heartbeats do not churn its WebSocket or decoder", () => {
test("healthy camera survives a long main-thread heartbeat gap without decoder churn", () => {
const authority = "camera-authority-acquisition-healthy";
let state = initialCameraPlaybackRecoveryState(authority, 1_000);
for (const now of [2_000, 3_000, 4_000, 5_000]) {
for (const now of [2_000, 3_000, 4_000, 12_000, 25_000]) {
const heartbeat = reduceCameraPlaybackRecovery(state, { type: "heartbeat" }, {
activeAuthorityIdentity: authority,
now,
documentVisible: true,
networkOnline: true,
transportHealthy: true,
});
assert.equal(heartbeat.reopen, false);
state = heartbeat.state;
@@ -295,6 +295,25 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
);
});
test("pending K1 STOP keeps the live Rerun source mounted until local capture ends", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
"utf8",
);
assert.match(
source,
/const intentionalSourceEnd = !recordedSource &&\s*state\?\.sourceMode === "idle" && \[/,
);
assert.match(
source,
/"awaiting_external_stop",\s*"stopping",\s*"finalizing",\s*"completed",/,
);
assert.doesNotMatch(
source,
/const intentionalSourceEnd = !recordedSource && \[\s*"awaiting_external_stop"/,
);
});
test("the spatial header reports raw replay as an active source", async () => {
const source = await readFile(
new URL("../src/App.tsx", import.meta.url),
@@ -16,6 +16,7 @@ let WorkspaceLayoutContractError;
let admitLiveDefaultPresentations;
let automaticLivePresentationIdentity;
let livePresentationCloseFence;
let restoredLayoutMayAdmitLiveDefault;
let observationPresentationSourceAfterLayoutApply;
let visibleSourceIdsAfterRecordedCatalogActivation;
@@ -39,6 +40,7 @@ before(async () => {
admitLiveDefaultPresentations,
automaticLivePresentationIdentity,
livePresentationCloseFence,
restoredLayoutMayAdmitLiveDefault,
observationPresentationSourceAfterLayoutApply,
visibleSourceIdsAfterRecordedCatalogActivation,
} = await server.ssrLoadModule("/src/core/observation/useObservationLayout.ts"));
@@ -308,6 +310,67 @@ test("a sequential live acquisition re-arms the same camera without reopening a
]);
});
test("a restored layout admits a selected delivery after reload and only same-acquisition successors", () => {
const camera = (acquisitionId, deliveryId) => ({
id: "k1:sensor.camera.right",
sourceId: "sensor.camera.right",
modality: "video",
availability: "streaming",
transport: "websocket",
previewUrl: null,
delivery: {
id: deliveryId,
kind: "mse-fmp4-websocket",
url: `/camera-preview/${deliveryId}`,
mediaType: 'video/mp4; codecs="avc1.641028"',
},
activation: {
groupId: "k1:device-session-reused:camera.preview.decoder",
maxActive: 1,
selected: true,
controllable: true,
},
binding: {
deviceId: "k1-a",
deviceSessionId: "device-session-reused",
acquisitionId,
},
capabilities: { defaultVisible: true, overlay: true },
});
const original = camera("acquisition-a", "camera-preview-2");
const successor = camera("acquisition-a", "camera-preview-3");
const unrelated = camera("acquisition-b", "camera-preview-3");
const presented = new Set([livePresentationCloseFence(original)]);
assert.equal(restoredLayoutMayAdmitLiveDefault([original], new Set()), true);
assert.equal(restoredLayoutMayAdmitLiveDefault([successor], presented), true);
assert.equal(restoredLayoutMayAdmitLiveDefault([unrelated], presented), false);
assert.equal(restoredLayoutMayAdmitLiveDefault([{
...original,
activation: { ...original.activation, selected: false },
}], new Set()), false);
const admitted = admitLiveDefaultPresentations(
[],
[successor],
new Set([automaticLivePresentationIdentity(original)]),
new Set(),
);
assert.deepEqual(admitted.visibleIds, [successor.id]);
assert.deepEqual(admitted.admittedIdentities, [
automaticLivePresentationIdentity(successor),
]);
const deliberatelyClosed = admitLiveDefaultPresentations(
[],
[successor],
new Set([automaticLivePresentationIdentity(original)]),
presented,
);
assert.deepEqual(deliberatelyClosed.visibleIds, []);
assert.deepEqual(deliberatelyClosed.admittedIdentities, []);
});
test("workspace layout API uses the canonical endpoint and optimistic revision", async () => {
const calls = [];
const current = decodeObservationWorkspaceLayoutProfile(wireProfile());