978 lines
35 KiB
JavaScript
978 lines
35 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { after, before, test } from "node:test";
|
||
|
||
import React, { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { createServer } from "vite";
|
||
|
||
let server;
|
||
let activeStreamForceFinishAuthority;
|
||
let activeStreamForceFinishAuthorityMatches;
|
||
let activeStreamRecoveredBrowserAuthority;
|
||
let activeStreamRecoveryPresentation;
|
||
let activeStreamRecoveryPresentationAuthority;
|
||
let activeStreamRecoveryOwnsPresentationDecision;
|
||
let exactActiveStreamRecoveryLineage;
|
||
let formatActiveStreamRecoveryElapsed;
|
||
let suppressGenericErrorDuringActiveStreamRecovery;
|
||
let isXgridsActiveStreamRecovery;
|
||
let K1AcquisitionPipeline;
|
||
let K1SpatialControlsView;
|
||
let runSpatialActiveStreamForceFinish;
|
||
let shouldRenderK1GenericRuntimeError;
|
||
let shouldRenderK1OperationalPanels;
|
||
let xgridsK1Actions;
|
||
let xgridsK1Api;
|
||
|
||
const hookSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
);
|
||
const acquisitionSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx",
|
||
import.meta.url,
|
||
);
|
||
const recoverySurfaceSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/ActiveStreamRecoverySurface.tsx",
|
||
import.meta.url,
|
||
);
|
||
const spatialControlsSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1SpatialControls.tsx",
|
||
import.meta.url,
|
||
);
|
||
const connectionSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx",
|
||
import.meta.url,
|
||
);
|
||
|
||
before(async () => {
|
||
server = await createServer({
|
||
appType: "custom",
|
||
logLevel: "silent",
|
||
server: { middlewareMode: true },
|
||
});
|
||
({
|
||
activeStreamForceFinishAuthority,
|
||
activeStreamForceFinishAuthorityMatches,
|
||
activeStreamRecoveredBrowserAuthority,
|
||
activeStreamRecoveryPresentation,
|
||
activeStreamRecoveryPresentationAuthority,
|
||
activeStreamRecoveryOwnsPresentationDecision,
|
||
exactActiveStreamRecoveryLineage,
|
||
formatActiveStreamRecoveryElapsed,
|
||
suppressGenericErrorDuringActiveStreamRecovery,
|
||
} = await server.ssrLoadModule("@xgrids-k1/frontend/activeStreamRecovery.ts"));
|
||
({ isXgridsActiveStreamRecovery, xgridsK1Api } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/api.ts",
|
||
));
|
||
({ xgridsK1Actions } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/manifest.ts",
|
||
));
|
||
({ K1AcquisitionPipeline } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1AcquisitionPipeline.tsx",
|
||
));
|
||
({
|
||
K1SpatialControlsView,
|
||
runSpatialActiveStreamForceFinish,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1SpatialControls.tsx",
|
||
));
|
||
({
|
||
shouldRenderK1GenericRuntimeError,
|
||
shouldRenderK1OperationalPanels,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/XgridsK1Connection.tsx",
|
||
));
|
||
});
|
||
|
||
after(async () => {
|
||
await server?.close();
|
||
});
|
||
|
||
function recoveryContract(overrides = {}) {
|
||
return {
|
||
schema_version: "missioncore.xgrids-k1-active-stream-recovery/v1",
|
||
state: "reconnecting",
|
||
generation: 7,
|
||
acquisition_id: "acquisition-recovery-001",
|
||
attempt: 3,
|
||
started_at_utc: "2026-08-11T19:31:00Z",
|
||
elapsed_ms: 12_400,
|
||
reason_code: "read-only-rebind-in-progress",
|
||
force_finish_allowed: true,
|
||
automatic_read_only_rebind: true,
|
||
automatic_command_retry: false,
|
||
start_performed: false,
|
||
stop_performed: false,
|
||
ble_operation_performed: false,
|
||
network_mutation_performed: false,
|
||
runtime_producer_generation: 11,
|
||
camera_recovery: "owned",
|
||
camera_media_state: "pending-first-media",
|
||
camera_media_ready: false,
|
||
camera_epoch: {
|
||
generation: 7,
|
||
init_committed: true,
|
||
init_committed_age_ms: 250,
|
||
first_media_committed: false,
|
||
first_media_committed_age_ms: null,
|
||
committed_media_segment_count: 0,
|
||
last_media_segment_age_ms: null,
|
||
},
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function recoveryState(recoveryOverrides = {}, stateOverrides = {}) {
|
||
return {
|
||
snapshot_runtime_id: "snapshot-runtime-recovery-001",
|
||
snapshot_revision: 43,
|
||
producer_generation: 11,
|
||
phase: "reconnecting",
|
||
source_mode: "live",
|
||
acquisition: {
|
||
acquisition_id: "acquisition-recovery-001",
|
||
device_id: "device-k1-001",
|
||
device_session_id: "device-session-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "plugin-commanded",
|
||
requested_streams: ["spatial.point-cloud.live"],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: 0,
|
||
evidence_policy: "required",
|
||
state: "acquiring",
|
||
state_revision: 9,
|
||
cleanup_pending: false,
|
||
},
|
||
connection_recovery: recoveryContract(recoveryOverrides),
|
||
...stateOverrides,
|
||
};
|
||
}
|
||
|
||
function coldRestartPrePclState() {
|
||
const state = recoveryState({
|
||
generation: 1,
|
||
acquisition_id: "acquisition-before-backend-restart-001",
|
||
attempt: 0,
|
||
started_at_utc: "2026-08-14T00:31:00Z",
|
||
elapsed_ms: 450,
|
||
reason_code: "restart-receiver-awaiting-first-pcl",
|
||
runtime_producer_generation: 1,
|
||
camera_recovery: "inactive",
|
||
camera_media_state: "inactive",
|
||
camera_media_ready: false,
|
||
camera_epoch: null,
|
||
}, {
|
||
snapshot_runtime_id: "snapshot-runtime-after-cold-restart-001",
|
||
snapshot_revision: 2,
|
||
producer_generation: 1,
|
||
acquisition: {
|
||
...recoveryState().acquisition,
|
||
acquisition_id: "acquisition-before-backend-restart-001",
|
||
state: "awaiting_external_start",
|
||
state_revision: 4,
|
||
},
|
||
camera_preview: {
|
||
activation_admission: {
|
||
state: "waiting-for-first-authoritative-pcl",
|
||
basis: "post-rerun-publish-pcl-frame",
|
||
runtime_producer_generation: 1,
|
||
device_command_sent: false,
|
||
},
|
||
},
|
||
});
|
||
return state;
|
||
}
|
||
|
||
function coldRestartRecoveredState() {
|
||
const state = coldRestartPrePclState();
|
||
return {
|
||
...state,
|
||
snapshot_revision: state.snapshot_revision + 1,
|
||
phase: "live",
|
||
acquisition: {
|
||
...state.acquisition,
|
||
state: "acquiring",
|
||
state_revision: state.acquisition.state_revision + 1,
|
||
},
|
||
connection_recovery: {
|
||
...state.connection_recovery,
|
||
state: "recovered",
|
||
elapsed_ms: null,
|
||
reason_code: null,
|
||
force_finish_allowed: false,
|
||
camera_recovery: "owned",
|
||
camera_media_state: "pending-epoch",
|
||
camera_media_ready: false,
|
||
camera_epoch: null,
|
||
},
|
||
camera_preview: {
|
||
activation_admission: {
|
||
state: "activating",
|
||
basis: "post-rerun-publish-pcl-frame",
|
||
runtime_producer_generation: 1,
|
||
device_command_sent: false,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function controller(state, overrides = {}) {
|
||
return {
|
||
state,
|
||
pendingAction: null,
|
||
error: null,
|
||
physicalStopIntentSpent: false,
|
||
physicalStopInFlight: false,
|
||
closeApplicationControlSession: async () => false,
|
||
prepareCanonicalAcquisition: async () => false,
|
||
startPreparedAcquisition: async () => false,
|
||
startReplay: async () => false,
|
||
stop: async () => false,
|
||
stopLocalReceiver: async () => false,
|
||
forceFinishActiveStreamLocally: async () => false,
|
||
abort: async () => false,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function renderPipeline(state, overrides = {}) {
|
||
return renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: controller(state, overrides),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
}
|
||
|
||
function renderSpatialControls(state, overrides = {}) {
|
||
return renderToStaticMarkup(createElement(K1SpatialControlsView, {
|
||
controller: controller(state, overrides),
|
||
}));
|
||
}
|
||
|
||
function buttonsWithText(markup, text) {
|
||
return (markup.match(/<button\b[\s\S]*?<\/button>/g) ?? [])
|
||
.filter((button) => button.includes(text));
|
||
}
|
||
|
||
function sourceSlice(source, startMarker, endMarker) {
|
||
const start = source.indexOf(startMarker);
|
||
const end = source.indexOf(endMarker, start + startMarker.length);
|
||
assert.notEqual(start, -1, `missing source marker: ${startMarker}`);
|
||
assert.notEqual(end, -1, `missing source marker: ${endMarker}`);
|
||
return source.slice(start, end);
|
||
}
|
||
|
||
test("active recovery contract is strict about all no-write invariants", () => {
|
||
assert.equal(isXgridsActiveStreamRecovery(recoveryContract()), true);
|
||
for (const field of [
|
||
"automatic_command_retry",
|
||
"start_performed",
|
||
"stop_performed",
|
||
"ble_operation_performed",
|
||
"network_mutation_performed",
|
||
]) {
|
||
assert.equal(
|
||
isXgridsActiveStreamRecovery(recoveryContract({ [field]: true })),
|
||
false,
|
||
field,
|
||
);
|
||
}
|
||
assert.equal(
|
||
isXgridsActiveStreamRecovery(recoveryContract({ state: "retrying-command" })),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
isXgridsActiveStreamRecovery(recoveryContract({ elapsed_ms: -1 })),
|
||
false,
|
||
);
|
||
const missingMediaState = recoveryContract();
|
||
delete missingMediaState.camera_media_state;
|
||
assert.equal(isXgridsActiveStreamRecovery(missingMediaState), false);
|
||
assert.equal(
|
||
isXgridsActiveStreamRecovery(recoveryContract({ camera_media_ready: true })),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
isXgridsActiveStreamRecovery(recoveryContract({
|
||
camera_media_state: "ready",
|
||
camera_media_ready: true,
|
||
camera_epoch: {
|
||
...recoveryContract().camera_epoch,
|
||
generation: 0,
|
||
first_media_committed: true,
|
||
first_media_committed_age_ms: 1,
|
||
committed_media_segment_count: 1,
|
||
last_media_segment_age_ms: 1,
|
||
},
|
||
})),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("cold restart before first PCL owns exact reconnect UI without camera or physical actions", () => {
|
||
const state = coldRestartPrePclState();
|
||
assert.equal(isXgridsActiveStreamRecovery(state.connection_recovery), true);
|
||
|
||
const lineage = exactActiveStreamRecoveryLineage(state);
|
||
assert.deepEqual(lineage && {
|
||
runtime: lineage.snapshotRuntimeId,
|
||
acquisition: lineage.acquisitionId,
|
||
revision: lineage.acquisitionStateRevision,
|
||
recovery: lineage.recoveryGeneration,
|
||
producer: lineage.runtimeProducerGeneration,
|
||
}, {
|
||
runtime: "snapshot-runtime-after-cold-restart-001",
|
||
acquisition: "acquisition-before-backend-restart-001",
|
||
revision: 4,
|
||
recovery: 1,
|
||
producer: 1,
|
||
});
|
||
assert.notEqual(activeStreamRecoveryPresentationAuthority(state), null);
|
||
assert.notEqual(activeStreamForceFinishAuthority(state), null);
|
||
assert.equal(activeStreamRecoveredBrowserAuthority(state), null);
|
||
assert.equal(state.connection_recovery.camera_recovery, "inactive");
|
||
assert.equal(state.connection_recovery.camera_media_state, "inactive");
|
||
assert.equal(state.connection_recovery.camera_media_ready, false);
|
||
assert.equal(state.connection_recovery.camera_epoch, null);
|
||
assert.deepEqual(state.camera_preview.activation_admission, {
|
||
state: "waiting-for-first-authoritative-pcl",
|
||
basis: "post-rerun-publish-pcl-frame",
|
||
runtime_producer_generation: 1,
|
||
device_command_sent: false,
|
||
});
|
||
|
||
const pipeline = renderPipeline(state);
|
||
assert.match(pipeline, /Восстанавливаем соединение/);
|
||
assert.match(pipeline, /START, STOP, Bluetooth и настройки устройства не отправляются/);
|
||
assert.equal(buttonsWithText(pipeline, "Прервать соединение").length, 1);
|
||
assert.equal(buttonsWithText(pipeline, "Запустить приём").length, 0);
|
||
assert.equal(buttonsWithText(pipeline, "Остановить устройство и запись").length, 0);
|
||
assert.equal(buttonsWithText(pipeline, "Остановить сканирование").length, 0);
|
||
assert.doesNotMatch(
|
||
pipeline,
|
||
/СВЯЗЬ ВОССТАНОВЛЕНА|Связь восстановлена · приём продолжается|Продолжаем тот же приём/,
|
||
);
|
||
|
||
const spatial = renderSpatialControls(state);
|
||
assert.match(spatial, /Восстанавливаем соединение/);
|
||
assert.equal(buttonsWithText(spatial, "Прервать соединение").length, 1);
|
||
assert.doesNotMatch(
|
||
spatial,
|
||
/Остановить устройство|Остановить K1|Завершить локальный приём/,
|
||
);
|
||
});
|
||
|
||
test("cold restart first PCL preserves lineage and renders recovered continuation copy", () => {
|
||
const beforePcl = coldRestartPrePclState();
|
||
const state = coldRestartRecoveredState();
|
||
assert.equal(isXgridsActiveStreamRecovery(state.connection_recovery), true);
|
||
assert.equal(
|
||
state.connection_recovery.acquisition_id,
|
||
beforePcl.connection_recovery.acquisition_id,
|
||
);
|
||
assert.equal(
|
||
state.connection_recovery.generation,
|
||
beforePcl.connection_recovery.generation,
|
||
);
|
||
assert.equal(
|
||
state.connection_recovery.runtime_producer_generation,
|
||
beforePcl.connection_recovery.runtime_producer_generation,
|
||
);
|
||
|
||
const lineage = exactActiveStreamRecoveryLineage(state);
|
||
const browserAuthority = activeStreamRecoveredBrowserAuthority(state);
|
||
assert.deepEqual(lineage && {
|
||
runtime: lineage.snapshotRuntimeId,
|
||
acquisition: lineage.acquisitionId,
|
||
revision: lineage.acquisitionStateRevision,
|
||
recovery: lineage.recoveryGeneration,
|
||
producer: lineage.runtimeProducerGeneration,
|
||
}, {
|
||
runtime: "snapshot-runtime-after-cold-restart-001",
|
||
acquisition: "acquisition-before-backend-restart-001",
|
||
revision: 5,
|
||
recovery: 1,
|
||
producer: 1,
|
||
});
|
||
assert.deepEqual(browserAuthority, lineage);
|
||
assert.equal(activeStreamRecoveryPresentationAuthority(state), null);
|
||
assert.equal(activeStreamForceFinishAuthority(state), null);
|
||
assert.equal(state.connection_recovery.camera_recovery, "owned");
|
||
assert.equal(state.camera_preview.activation_admission.device_command_sent, false);
|
||
|
||
const pipeline = renderPipeline(state);
|
||
assert.doesNotMatch(pipeline, /Восстанавливаем соединение|Прервать соединение/);
|
||
assert.match(pipeline, /СВЯЗЬ ВОССТАНОВЛЕНА · АКТИВНЫЙ ПРИЁМ/);
|
||
assert.match(pipeline, /Связь восстановлена · приём продолжается/);
|
||
assert.match(pipeline, /Продолжаем тот же приём без нового START/);
|
||
assert.doesNotMatch(pipeline, /Назовите проект и запустите приём|Запустить приём/);
|
||
});
|
||
|
||
test("force-finish authority requires exact runtime, acquisition, revision and generations", () => {
|
||
const state = recoveryState();
|
||
const authority = activeStreamForceFinishAuthority(state);
|
||
assert.deepEqual(authority && {
|
||
snapshotRuntimeId: authority.snapshotRuntimeId,
|
||
acquisitionId: authority.acquisitionId,
|
||
acquisitionStateRevision: authority.acquisitionStateRevision,
|
||
recoveryGeneration: authority.recoveryGeneration,
|
||
runtimeProducerGeneration: authority.runtimeProducerGeneration,
|
||
}, {
|
||
snapshotRuntimeId: "snapshot-runtime-recovery-001",
|
||
acquisitionId: "acquisition-recovery-001",
|
||
acquisitionStateRevision: 9,
|
||
recoveryGeneration: 7,
|
||
runtimeProducerGeneration: 11,
|
||
});
|
||
assert.equal(activeStreamForceFinishAuthorityMatches(authority, state), true);
|
||
|
||
const staleCases = [
|
||
(() => {
|
||
const value = structuredClone(state);
|
||
value.snapshot_runtime_id = "snapshot-runtime-recovery-002";
|
||
return value;
|
||
})(),
|
||
(() => {
|
||
const value = structuredClone(state);
|
||
value.acquisition.acquisition_id = "acquisition-recovery-002";
|
||
return value;
|
||
})(),
|
||
(() => {
|
||
const value = structuredClone(state);
|
||
value.acquisition.state_revision += 1;
|
||
return value;
|
||
})(),
|
||
(() => {
|
||
const value = structuredClone(state);
|
||
value.connection_recovery.generation += 1;
|
||
return value;
|
||
})(),
|
||
(() => {
|
||
const value = structuredClone(state);
|
||
value.producer_generation += 1;
|
||
return value;
|
||
})(),
|
||
];
|
||
for (const stale of staleCases) {
|
||
assert.equal(activeStreamForceFinishAuthorityMatches(authority, stale), false);
|
||
}
|
||
|
||
assert.equal(
|
||
activeStreamForceFinishAuthority(recoveryState({ state: "recovered", force_finish_allowed: false })),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
activeStreamForceFinishAuthority(recoveryState({ force_finish_allowed: false })),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("reconnecting presentation is exact and owns stale supervisor projection", () => {
|
||
const state = recoveryState({ force_finish_allowed: false });
|
||
const authority = activeStreamRecoveryPresentationAuthority(state);
|
||
assert.deepEqual(authority && {
|
||
runtime: authority.snapshotRuntimeId,
|
||
acquisition: authority.acquisitionId,
|
||
producer: authority.runtimeProducerGeneration,
|
||
recovery: authority.recoveryGeneration,
|
||
}, {
|
||
runtime: "snapshot-runtime-recovery-001",
|
||
acquisition: "acquisition-recovery-001",
|
||
producer: 11,
|
||
recovery: 7,
|
||
});
|
||
assert.equal(activeStreamRecoveryOwnsPresentationDecision(state), true);
|
||
|
||
const wrongProducer = structuredClone(state);
|
||
wrongProducer.producer_generation += 1;
|
||
assert.equal(activeStreamRecoveryPresentationAuthority(wrongProducer), null);
|
||
assert.equal(
|
||
activeStreamRecoveryOwnsPresentationDecision(wrongProducer),
|
||
true,
|
||
"a valid reconnect contract must block stale ordinary data projection",
|
||
);
|
||
|
||
for (const recoveryStateName of [
|
||
"blocked",
|
||
"standby",
|
||
"fault",
|
||
"force-finishing",
|
||
"force-finished",
|
||
]) {
|
||
const terminal = recoveryState({ state: recoveryStateName });
|
||
assert.equal(activeStreamRecoveryPresentationAuthority(terminal), null);
|
||
assert.equal(activeStreamRecoveryOwnsPresentationDecision(terminal), true);
|
||
}
|
||
assert.equal(
|
||
activeStreamRecoveryOwnsPresentationDecision(recoveryState({ state: "recovered" })),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("recovered browser carryover keeps exact lineage without restoring recovery controls", () => {
|
||
const state = recoveryState({
|
||
state: "recovered",
|
||
force_finish_allowed: false,
|
||
elapsed_ms: null,
|
||
reason_code: null,
|
||
}, {
|
||
phase: "live",
|
||
});
|
||
const authority = activeStreamRecoveredBrowserAuthority(state);
|
||
assert.deepEqual(authority && {
|
||
runtime: authority.snapshotRuntimeId,
|
||
acquisition: authority.acquisitionId,
|
||
revision: authority.acquisitionStateRevision,
|
||
producer: authority.runtimeProducerGeneration,
|
||
recovery: authority.recoveryGeneration,
|
||
}, {
|
||
runtime: "snapshot-runtime-recovery-001",
|
||
acquisition: "acquisition-recovery-001",
|
||
revision: 9,
|
||
producer: 11,
|
||
recovery: 7,
|
||
});
|
||
assert.equal(activeStreamRecoveryPresentation(state), null);
|
||
assert.equal(activeStreamForceFinishAuthority(state), null);
|
||
|
||
const staleProducer = structuredClone(state);
|
||
staleProducer.producer_generation += 1;
|
||
assert.equal(activeStreamRecoveredBrowserAuthority(staleProducer), null);
|
||
assert.equal(
|
||
activeStreamRecoveredBrowserAuthority(recoveryState({
|
||
state: "recovered",
|
||
camera_recovery: "blocked",
|
||
force_finish_allowed: false,
|
||
}, { phase: "live" })),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("only an exact reconnecting lineage suppresses the generic red error", () => {
|
||
const state = recoveryState();
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(state, null), true);
|
||
assert.equal(
|
||
shouldRenderK1GenericRuntimeError("Ошибка локального приёмника", false, state, null),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
suppressGenericErrorDuringActiveStreamRecovery(state, "force-finish"),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
shouldRenderK1GenericRuntimeError(
|
||
"Локальное завершение не выполнено",
|
||
false,
|
||
state,
|
||
"force-finish",
|
||
),
|
||
true,
|
||
"a failed explicit local finish must keep the generic error banner visible",
|
||
);
|
||
|
||
const wrongProducer = structuredClone(state);
|
||
wrongProducer.producer_generation += 1;
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(wrongProducer), false);
|
||
|
||
const wrongAcquisition = structuredClone(state);
|
||
wrongAcquisition.connection_recovery.acquisition_id = "acquisition-stale";
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(wrongAcquisition), false);
|
||
|
||
const missingRuntime = structuredClone(state);
|
||
delete missingRuntime.snapshot_runtime_id;
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(missingRuntime), false);
|
||
|
||
const blocked = recoveryState({ state: "blocked" });
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(blocked), false);
|
||
|
||
const nonOwned = recoveryState({ automatic_read_only_rebind: false });
|
||
assert.equal(suppressGenericErrorDuringActiveStreamRecovery(nonOwned), false);
|
||
|
||
const inactiveCases = [
|
||
recoveryState({}, { phase: "error" }),
|
||
recoveryState({}, { source_mode: "idle" }),
|
||
recoveryState({}, {
|
||
acquisition: {
|
||
...state.acquisition,
|
||
state: "failed",
|
||
},
|
||
}),
|
||
];
|
||
for (const inactive of inactiveCases) {
|
||
assert.equal(
|
||
suppressGenericErrorDuringActiveStreamRecovery(inactive, null),
|
||
false,
|
||
"stale recovery projection must fail open to the error banner",
|
||
);
|
||
}
|
||
});
|
||
|
||
test("reconnecting presentation is neutral, timed and exposes explicit local finish", () => {
|
||
const state = recoveryState();
|
||
const presentation = activeStreamRecoveryPresentation(state);
|
||
assert.equal(presentation?.state, "reconnecting");
|
||
assert.equal(presentation?.tone, "neutral");
|
||
assert.equal(presentation?.progressLabel, "Попытка 3 · 12 с");
|
||
assert.equal(presentation?.forceFinishAvailable, true);
|
||
|
||
const markup = renderPipeline(state);
|
||
assert.match(markup, /Восстанавливаем соединение/);
|
||
assert.match(markup, /Попытка 3 · 12 с/);
|
||
assert.match(markup, /class="nodedc-activity-indicator/);
|
||
assert.equal(buttonsWithText(markup, "Прервать соединение").length, 1);
|
||
assert.match(markup, /START, STOP, Bluetooth и настройки устройства не отправляются/);
|
||
assert.doesNotMatch(markup, /Ошибка локального приёмника/);
|
||
assert.equal(shouldRenderK1OperationalPanels(state), true);
|
||
});
|
||
|
||
test("spatial scene owns the same recovery spinner and explicit local finish", () => {
|
||
const markup = renderSpatialControls(recoveryState());
|
||
assert.match(markup, /Восстанавливаем соединение/);
|
||
assert.match(markup, /Попытка 3 · 12 с/);
|
||
assert.match(markup, /class="nodedc-activity-indicator/);
|
||
assert.match(markup, /data-recovery-state="reconnecting"/);
|
||
assert.equal(buttonsWithText(markup, "Прервать соединение").length, 1);
|
||
assert.match(markup, /локальный front\/back-приём/);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Остановить устройство|Остановить K1|Завершить локальный приём/,
|
||
"recovery must not expose canonical STOP or the generic receiver stop",
|
||
);
|
||
});
|
||
|
||
test("spatial blocked and fault recovery copy is terminal and truthful", () => {
|
||
const blocked = renderSpatialControls(recoveryState({
|
||
state: "blocked",
|
||
reason_code: "exact-binding-changed",
|
||
}));
|
||
assert.match(blocked, /Связь не восстановлена/);
|
||
assert.match(blocked, /Восстановление остановлено/);
|
||
assert.doesNotMatch(blocked, /class="nodedc-activity-indicator/);
|
||
assert.equal(buttonsWithText(blocked, "Прервать соединение").length, 1);
|
||
|
||
const fault = renderSpatialControls(recoveryState({
|
||
state: "fault",
|
||
force_finish_allowed: false,
|
||
reason_code: "active-stream-recovery-system-error",
|
||
}));
|
||
assert.match(fault, /K1 сообщил об ошибке/);
|
||
assert.match(fault, /Автоматических команд и повторов нет/);
|
||
assert.equal(buttonsWithText(fault, "Прервать соединение").length, 0);
|
||
});
|
||
|
||
test("spatial recovery interaction routes only to exact local force-finish", async () => {
|
||
let forceFinishCalls = 0;
|
||
const current = recoveryState();
|
||
const invoked = await runSpatialActiveStreamForceFinish({
|
||
state: current,
|
||
forceFinishActiveStreamLocally: async () => {
|
||
forceFinishCalls += 1;
|
||
return true;
|
||
},
|
||
});
|
||
assert.equal(invoked, true);
|
||
assert.equal(forceFinishCalls, 1);
|
||
|
||
const stale = structuredClone(current);
|
||
stale.connection_recovery.runtime_producer_generation += 1;
|
||
const rejected = await runSpatialActiveStreamForceFinish({
|
||
state: stale,
|
||
forceFinishActiveStreamLocally: async () => {
|
||
forceFinishCalls += 1;
|
||
return true;
|
||
},
|
||
});
|
||
assert.equal(rejected, false);
|
||
assert.equal(forceFinishCalls, 1, "stale lineage must not dispatch any action");
|
||
});
|
||
|
||
test("spatial force-finish pending owns the surface without a second action", () => {
|
||
const markup = renderSpatialControls(
|
||
recoveryState({
|
||
state: "force-finishing",
|
||
acquisition_id: null,
|
||
force_finish_allowed: false,
|
||
automatic_read_only_rebind: false,
|
||
camera_recovery: "inactive",
|
||
}),
|
||
{ pendingAction: "force-finish" },
|
||
);
|
||
assert.match(markup, /Завершение локального приёма/);
|
||
assert.match(markup, /data-recovery-state="force-finishing"/);
|
||
assert.match(markup, /Команда STOP устройству не отправляется/);
|
||
assert.equal(buttonsWithText(markup, "Прервать соединение").length, 0);
|
||
});
|
||
|
||
test("blocked, camera-blocked, standby and fault copy stay truthful", () => {
|
||
const blocked = recoveryState({ state: "blocked", reason_code: "exact-binding-changed" });
|
||
const blockedMarkup = renderPipeline(blocked);
|
||
assert.match(blockedMarkup, /Связь не восстановлена/);
|
||
assert.match(blockedMarkup, /Восстановление остановлено/);
|
||
assert.doesNotMatch(blockedMarkup, /class="nodedc-activity-indicator/);
|
||
assert.equal(buttonsWithText(blockedMarkup, "Прервать соединение").length, 1);
|
||
|
||
const cameraBlocked = recoveryState({
|
||
state: "blocked",
|
||
camera_recovery: "blocked",
|
||
reason_code: "camera-recovery-failed",
|
||
});
|
||
assert.match(renderPipeline(cameraBlocked), /Видеопоток не восстановлен/);
|
||
|
||
const standby = recoveryState({
|
||
state: "standby",
|
||
force_finish_allowed: false,
|
||
reason_code: "device-reported-standby",
|
||
});
|
||
const standbyMarkup = renderPipeline(standby);
|
||
assert.match(standbyMarkup, /Устройство перешло в ожидание/);
|
||
assert.match(standbyMarkup, /без команды STOP/);
|
||
assert.equal(buttonsWithText(standbyMarkup, "Прервать соединение").length, 0);
|
||
assert.equal(shouldRenderK1OperationalPanels(standby), true);
|
||
|
||
const fault = recoveryState({
|
||
state: "fault",
|
||
force_finish_allowed: false,
|
||
reason_code: "active-stream-recovery-system-error",
|
||
});
|
||
const faultMarkup = renderPipeline(fault);
|
||
assert.match(faultMarkup, /K1 сообщил об ошибке/);
|
||
assert.match(faultMarkup, /Автоматических команд и повторов нет/);
|
||
assert.equal(buttonsWithText(faultMarkup, "Прервать соединение").length, 0);
|
||
assert.equal(shouldRenderK1OperationalPanels(fault), true);
|
||
});
|
||
|
||
test("recovered active lineage renders the continued session and one exact STOP", () => {
|
||
const state = recoveryState({
|
||
state: "recovered",
|
||
force_finish_allowed: false,
|
||
elapsed_ms: null,
|
||
reason_code: null,
|
||
}, {
|
||
phase: "live",
|
||
compatibility: {
|
||
vendor_writes_enabled: true,
|
||
permitted_mode: "active-control",
|
||
},
|
||
application_control_session: {
|
||
session_generation: 5,
|
||
state_revision: 8,
|
||
state: "scanning",
|
||
can_stop: true,
|
||
control_socket_open: true,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["stop-acquisition"],
|
||
actions: {
|
||
"stop-acquisition": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
assert.equal(activeStreamRecoveryPresentation(state), null);
|
||
const markup = renderPipeline(state);
|
||
assert.doesNotMatch(markup, /Восстанавливаем соединение|Прервать соединение/);
|
||
assert.match(markup, /СВЯЗЬ ВОССТАНОВЛЕНА · АКТИВНЫЙ ПРИЁМ/);
|
||
assert.match(markup, /Связь восстановлена · приём продолжается/);
|
||
assert.match(markup, /Продолжаем тот же приём без нового START/);
|
||
assert.doesNotMatch(markup, /Назовите проект и запустите приём|Запустить приём/);
|
||
|
||
const stopButtons = buttonsWithText(markup, "Остановить устройство и запись");
|
||
assert.equal(stopButtons.length, 1);
|
||
assert.doesNotMatch(stopButtons[0], /\bdisabled(?:=|\s|>)/);
|
||
});
|
||
|
||
test("stale recovered marker on an idle released runtime fails closed to idle UI", () => {
|
||
const state = recoveryState({
|
||
state: "recovered",
|
||
force_finish_allowed: false,
|
||
elapsed_ms: null,
|
||
reason_code: null,
|
||
}, {
|
||
phase: "idle",
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
...recoveryState().acquisition,
|
||
state: "completed",
|
||
cleanup_pending: false,
|
||
},
|
||
application_control_session: {
|
||
session_generation: 5,
|
||
state_revision: 9,
|
||
state: "completed",
|
||
can_stop: false,
|
||
control_socket_open: false,
|
||
},
|
||
});
|
||
assert.equal(activeStreamRecoveryPresentation(state), null);
|
||
const markup = renderPipeline(state);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/СВЯЗЬ ВОССТАНОВЛЕНА|Связь восстановлена · приём продолжается|Продолжаем тот же приём/,
|
||
);
|
||
assert.match(markup, /Назовите проект и запустите приём/);
|
||
assert.equal(buttonsWithText(markup, "Остановить устройство и запись").length, 0);
|
||
assert.equal(buttonsWithText(markup, "Остановить сканирование").length, 0);
|
||
});
|
||
|
||
test("force-finishing shows one local-only pending owner and no second action", () => {
|
||
const state = recoveryState({
|
||
state: "force-finishing",
|
||
acquisition_id: null,
|
||
force_finish_allowed: false,
|
||
automatic_read_only_rebind: false,
|
||
runtime_producer_generation: 11,
|
||
camera_recovery: "inactive",
|
||
});
|
||
const markup = renderPipeline(state, { pendingAction: "force-finish" });
|
||
assert.match(markup, /Завершение локального приёма/);
|
||
assert.match(markup, /Команда STOP устройству не отправляется/);
|
||
assert.match(markup, /class="nodedc-activity-indicator/);
|
||
assert.equal(buttonsWithText(markup, "Прервать соединение").length, 0);
|
||
});
|
||
|
||
test("elapsed presentation is deterministic", () => {
|
||
assert.equal(formatActiveStreamRecoveryElapsed(null), null);
|
||
assert.equal(formatActiveStreamRecoveryElapsed(-1), null);
|
||
assert.equal(formatActiveStreamRecoveryElapsed(999), "0 с");
|
||
assert.equal(formatActiveStreamRecoveryElapsed(59_999), "59 с");
|
||
assert.equal(formatActiveStreamRecoveryElapsed(60_000), "1 мин");
|
||
assert.equal(formatActiveStreamRecoveryElapsed(125_900), "2 мин 5 с");
|
||
});
|
||
|
||
test("force-finish manifest/API sends the exact fenced local-only request", async () => {
|
||
assert.equal(
|
||
xgridsK1Actions.acquisitionForceFinishLocal,
|
||
"acquisition.force-finish-local",
|
||
);
|
||
const request = {
|
||
expected_snapshot_runtime_id: "snapshot-runtime-recovery-001",
|
||
acquisition_id: "acquisition-recovery-001",
|
||
expected_state_revision: 9,
|
||
expected_recovery_generation: 7,
|
||
operator_confirmed: true,
|
||
operation_id: "op-00000000-0000-4000-8000-000000000321",
|
||
idempotency_key:
|
||
"acquisition.force-finish-local:op-00000000-0000-4000-8000-000000000321",
|
||
deadline_seconds: 30,
|
||
};
|
||
let capturedUrl = null;
|
||
let capturedInit = null;
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async (input, init) => {
|
||
capturedUrl = String(input);
|
||
capturedInit = init;
|
||
return new Response(JSON.stringify({ state: recoveryState() }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
};
|
||
try {
|
||
await xgridsK1Api.forceFinishAcquisitionLocally(request);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
assert.match(
|
||
capturedUrl,
|
||
/\/actions\/acquisition\.force-finish-local$/,
|
||
);
|
||
assert.equal(capturedInit.method, "POST");
|
||
assert.deepEqual(JSON.parse(capturedInit.body), { input: request });
|
||
});
|
||
|
||
test("state API rejects a drifted active recovery contract", async () => {
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||
state: recoveryState({ stop_performed: true }),
|
||
}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
try {
|
||
await assert.rejects(
|
||
() => xgridsK1Api.getState(),
|
||
/некорректное состояние/,
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("frontend boundary keeps recovery automatic work read-only and local finish explicit", () => {
|
||
const hookSource = readFileSync(hookSourceUrl, "utf8");
|
||
const forceFinish = sourceSlice(
|
||
hookSource,
|
||
"const forceFinishActiveStreamLocally",
|
||
"const abort",
|
||
);
|
||
assert.match(forceFinish, /run\("force-finish"/);
|
||
assert.match(forceFinish, /activeStreamForceFinishAuthority\(latestState\.current\)/);
|
||
assert.match(forceFinish, /expected_snapshot_runtime_id:\s*authority\.snapshotRuntimeId/);
|
||
assert.match(forceFinish, /acquisition_id:\s*authority\.acquisitionId/);
|
||
assert.match(forceFinish, /expected_state_revision:\s*authority\.acquisitionStateRevision/);
|
||
assert.match(forceFinish, /expected_recovery_generation:\s*authority\.recoveryGeneration/);
|
||
assert.match(forceFinish, /operator_confirmed:\s*true/);
|
||
assert.match(
|
||
forceFinish,
|
||
/newMutationContext\("acquisition\.force-finish-local"\)/,
|
||
);
|
||
assert.equal(
|
||
(forceFinish.match(/forceFinishAcquisitionLocally\(/g) ?? []).length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
forceFinish,
|
||
/startAcquisition|stopAcquisition|scanBle|selectCameraPreview|connect\(/,
|
||
);
|
||
|
||
const acquisitionSource = readFileSync(acquisitionSourceUrl, "utf8");
|
||
assert.equal(
|
||
(acquisitionSource.match(/forceFinishActiveStreamLocally\(\)/g) ?? []).length,
|
||
1,
|
||
"the explicit recovery button is the only frontend caller",
|
||
);
|
||
const recoverySurface = readFileSync(recoverySurfaceSourceUrl, "utf8");
|
||
assert.match(recoverySurface, /<ActivityIndicator/);
|
||
assert.match(recoverySurface, /<Button[\s\S]*?variant="secondary"/);
|
||
assert.doesNotMatch(recoverySurface, /<button\b|style=\{/);
|
||
|
||
const spatialSource = readFileSync(spatialControlsSourceUrl, "utf8");
|
||
assert.match(spatialSource, /variant="compact"/);
|
||
assert.match(
|
||
spatialSource,
|
||
/runSpatialActiveStreamForceFinish\(\{[\s\S]*?forceFinishActiveStreamLocally/,
|
||
);
|
||
const spatialForceFinish = sourceSlice(
|
||
spatialSource,
|
||
"export function runSpatialActiveStreamForceFinish",
|
||
"export function K1SpatialControlsView",
|
||
);
|
||
assert.match(spatialForceFinish, /activeStreamForceFinishAuthority\(controller\.state\)/);
|
||
assert.equal(
|
||
(spatialForceFinish.match(/controller\.forceFinishActiveStreamLocally\(\)/g) ?? []).length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(spatialForceFinish, /\bstop\(|stopLocalReceiver|start|connect|scanBle/);
|
||
|
||
const connectionSource = readFileSync(connectionSourceUrl, "utf8");
|
||
assert.match(
|
||
connectionSource,
|
||
/shouldRenderK1GenericRuntimeError\([\s\S]*?errorCorrelation\?\.action/,
|
||
);
|
||
assert.match(
|
||
connectionSource,
|
||
/\{showGenericRuntimeError && error \? \([\s\S]*?<K1OperatorError/,
|
||
);
|
||
});
|