chore(k1): checkpoint connection recovery work
This commit is contained in:
@@ -292,6 +292,21 @@ export function isLiveRerunPresentationReady(
|
||||
isUsableRecordedPlaybackRange(rangeNs);
|
||||
}
|
||||
|
||||
/**
|
||||
* `recording_open` can arrive before Rerun has registered the live timeline.
|
||||
* Selecting it at that point is a silent no-op, so keep retrying only until
|
||||
* the exact live timeline is both available and active.
|
||||
*/
|
||||
export function liveTimelineNeedsSynchronization(
|
||||
followLive: boolean,
|
||||
activeTimeline: string | null | undefined,
|
||||
rangeNs: { min: number; max: number } | null,
|
||||
): boolean {
|
||||
return followLive &&
|
||||
isUsableRecordedPlaybackRange(rangeNs) &&
|
||||
activeTimeline !== "stream_time";
|
||||
}
|
||||
|
||||
/**
|
||||
* Key the native receiver to its data-plane binding. Recovery authority is a
|
||||
* retry fence projected from changing supervisor snapshots; it must not tear
|
||||
@@ -1435,6 +1450,7 @@ export function RerunViewport({
|
||||
let rangeNs = viewer.get_time_range(event.recording_id, timeline);
|
||||
let currentNs = viewer.get_current_time(event.recording_id, timeline);
|
||||
let playing = viewer.get_playing(event.recording_id);
|
||||
let liveTimelineSynchronized = !followLive;
|
||||
if (!followLive && playing) {
|
||||
try {
|
||||
viewer.set_playing(event.recording_id, false);
|
||||
@@ -1496,6 +1512,17 @@ export function RerunViewport({
|
||||
if (disposed || !playbackState) return;
|
||||
try {
|
||||
rangeNs = viewer.get_time_range(event.recording_id, timeline);
|
||||
if (followLive && !liveTimelineSynchronized) {
|
||||
let activeTimeline = viewer.get_active_timeline(event.recording_id);
|
||||
if (liveTimelineNeedsSynchronization(followLive, activeTimeline, rangeNs)) {
|
||||
// The first selection above may have raced timeline
|
||||
// creation. Once a real range exists this retry is safe
|
||||
// and preserves Rerun's native live-following cursor.
|
||||
viewer.set_active_timeline(event.recording_id, timeline);
|
||||
activeTimeline = viewer.get_active_timeline(event.recording_id);
|
||||
}
|
||||
liveTimelineSynchronized = activeTimeline === timeline;
|
||||
}
|
||||
currentNs = viewer.get_current_time(event.recording_id, timeline);
|
||||
playing = viewer.get_playing(event.recording_id);
|
||||
} catch {
|
||||
@@ -1511,7 +1538,7 @@ export function RerunViewport({
|
||||
setRecordingBufferProgress(recordedBuffer.bufferProgress);
|
||||
}
|
||||
const readyToRender = followLive
|
||||
? isLiveRerunPresentationReady(
|
||||
? liveTimelineSynchronized && isLiveRerunPresentationReady(
|
||||
viewerStartResolved,
|
||||
rangeNs,
|
||||
liveActivitySequenceRef.current,
|
||||
|
||||
@@ -157,6 +157,7 @@ function SpatialWorkspace({
|
||||
const [showSegmentation, setShowSegmentation] = useState(false);
|
||||
const [showCuboids3d, setShowCuboids3d] = useState(false);
|
||||
const recordedSource = Boolean(recordedReplay) || /\.rrd(?:$|[?#])/i.test(sourceUrl);
|
||||
const liveRerunSource = !recordedSource && /^rerun\+https?:\/\//i.test(sourceUrl.trim());
|
||||
const recordedSessionGate: RecordedAdmissionPhase = recordedSource
|
||||
? recordedSessionAdmission?.phase ?? "loading"
|
||||
: "ready";
|
||||
@@ -165,6 +166,10 @@ function SpatialWorkspace({
|
||||
isRecordedPlaybackPresentationReady(viewerStatus, playbackState));
|
||||
const streamActive = state?.sourceMode === "live" || state?.sourceMode === "replay";
|
||||
const metrics = streamActive ? state?.metrics : undefined;
|
||||
// An explicit manual gRPC source has no Mission Core metrics producer. Its
|
||||
// native Rerun range is therefore the only available activity proof.
|
||||
const livePresentationActivitySequence = metrics?.publishedFrameCount ??
|
||||
(liveRerunSource && !streamActive ? 1 : null);
|
||||
const latency = pipelineLatency(metrics);
|
||||
const frameRate = finiteMetric(metrics?.frameRateHz);
|
||||
const points = finiteMetric(metrics?.pointCount);
|
||||
@@ -495,8 +500,8 @@ function SpatialWorkspace({
|
||||
<RerunViewport
|
||||
sourceUrl={sourceUrl}
|
||||
recordedArtifact={recordedSource ? recordedReplay : null}
|
||||
followLive={!recordedReplay && streamActive}
|
||||
liveActivitySequence={metrics?.publishedFrameCount}
|
||||
followLive={liveRerunSource}
|
||||
liveActivitySequence={livePresentationActivitySequence}
|
||||
liveStreamId={state?.spatialSource?.id}
|
||||
liveRecoveryAuthorityIdentity={!recordedSource && streamActive ? liveRerunRecoveryAuthorityIdentity(pointCloudSource, state?.spatialSource) : null}
|
||||
autoplayWhenReady={recordedSource}
|
||||
|
||||
@@ -590,7 +590,14 @@ test("canonical K1 preparation stops before START and guards every async stage",
|
||||
canonicalPreparation.match(
|
||||
/expected_snapshot_runtime_id: actionSnapshotRuntimeId/g,
|
||||
)?.length,
|
||||
3,
|
||||
4,
|
||||
);
|
||||
assert.match(canonicalPreparation, /xgridsK1Api\.openApplicationControlSession\(\{/);
|
||||
assert.match(canonicalPreparation, /\.\.\.request\.physicalAcceptance/);
|
||||
assert.ok(
|
||||
canonicalPreparation.indexOf("xgridsK1Api.openApplicationControlSession")
|
||||
< canonicalPreparation.indexOf("xgridsK1Api.enterApplicationWorkspace"),
|
||||
"Verify inspection must be replaced before workspace entry",
|
||||
);
|
||||
assert.match(canonicalPreparation, /xgridsK1Api\.reconcilePhysicalCommand\(\{/);
|
||||
assert.doesNotMatch(canonicalPreparation, /await xgridsK1Api\./);
|
||||
|
||||
@@ -308,7 +308,7 @@ test("connected presentation uses canonical process copy", () => {
|
||||
assert.match(connection, /<h2>Подключение \{model\.displayName\}<\/h2>/);
|
||||
assert.match(
|
||||
connection,
|
||||
/const operationalPanelsVisible = shouldRenderK1OperationalPanels\(state\)/,
|
||||
/const operationalPanelsVisible = shouldRenderK1OperationalPanels\(\s*state,\s*controller\.pendingAction,\s*\)/,
|
||||
);
|
||||
assert.match(
|
||||
connection,
|
||||
@@ -395,7 +395,7 @@ test("K1 START renders an in-button spinner for the complete live orchestration"
|
||||
assert.doesNotMatch(acquisition, /readOnlyConnectionObservationTarget\(state\)/);
|
||||
assert.match(acquisition, /appliedTopology\?\.status === "active"\s*&& desiredModeMatchesActive/);
|
||||
assert.match(acquisition, /if \(!draftPreparationTarget\) return;/);
|
||||
assert.match(acquisition, /START не используется для установки связи/);
|
||||
assert.doesNotMatch(acquisition, /START не используется для установки связи/);
|
||||
assert.match(acquisition, /Синхронизация…/);
|
||||
});
|
||||
|
||||
@@ -502,7 +502,7 @@ test("K1 mode reset is explicit while Scan and Apply keep exact backend CAS", ()
|
||||
assert.match(acquisition, /state\?\.active_connection_mode/);
|
||||
assert.match(acquisition, /desiredSelectionCommitted/);
|
||||
assert.match(acquisition, /configuredConnectionMode !== desiredConnectionMode/);
|
||||
assert.match(acquisition, /Выбран другой способ связи/);
|
||||
assert.doesNotMatch(acquisition, /Выбран другой способ связи/);
|
||||
});
|
||||
|
||||
test("top-right device utility is an explicit pending-aware K1 scenario reset", async () => {
|
||||
@@ -990,7 +990,7 @@ test("every K1 connection and acquisition action is fenced to the accepted backe
|
||||
[...runtime.matchAll(
|
||||
/expected_snapshot_runtime_id: actionSnapshotRuntimeId/g,
|
||||
)].length,
|
||||
3,
|
||||
4,
|
||||
);
|
||||
assert.match(
|
||||
runtime,
|
||||
@@ -1064,7 +1064,7 @@ test("K1 orchestration accepts backend recovery but still requires exact topolog
|
||||
);
|
||||
assert.match(acquisition, /desiredModeMatchesActive/);
|
||||
assert.match(acquisition, /modeSwitchRequired/);
|
||||
assert.match(acquisition, /Выбран другой способ связи/);
|
||||
assert.doesNotMatch(acquisition, /Выбран другой способ связи/);
|
||||
assert.match(acquisition, /prepareCanonicalAcquisition/);
|
||||
assert.match(acquisition, /startPreparedAcquisition/);
|
||||
assert.match(acquisition, /operatorActionPhysicalAcceptance\(\)/);
|
||||
|
||||
@@ -65,6 +65,7 @@ let shellPresentation;
|
||||
let reconfigurationAllowsFreshDevice;
|
||||
let readOnlyObservationShowsNetworkUnavailable;
|
||||
let readOnlyFailureShowsNetworkUnavailable;
|
||||
let savedBridgeRequiresNetworkSetup;
|
||||
let provisioningFailureRequiresFreshCandidate;
|
||||
let trustedConnectionBinding;
|
||||
let transportRefEquivalenceKey;
|
||||
@@ -183,6 +184,7 @@ before(async () => {
|
||||
reconfigurationAllowsFreshDevice,
|
||||
readOnlyObservationShowsNetworkUnavailable,
|
||||
readOnlyFailureShowsNetworkUnavailable,
|
||||
savedBridgeRequiresNetworkSetup,
|
||||
provisioningFailureRequiresFreshCandidate,
|
||||
trustedConnectionBinding,
|
||||
transportRefEquivalenceKey,
|
||||
@@ -1057,12 +1059,15 @@ function connectionPipelinesController(state) {
|
||||
};
|
||||
}
|
||||
|
||||
function renderConnectionPipelines(state) {
|
||||
function renderConnectionPipelines(state, pendingAction = null) {
|
||||
return renderToStaticMarkup(createElement(K1ConnectionPipelines, {
|
||||
controller: connectionPipelinesController(state),
|
||||
controller: {
|
||||
...connectionPipelinesController(state),
|
||||
pendingAction,
|
||||
},
|
||||
desiredConnectionMode: "bridge",
|
||||
onDesiredConnectionModeChange() {},
|
||||
operationalPanelsVisible: shouldRenderK1OperationalPanels(state),
|
||||
operationalPanelsVisible: shouldRenderK1OperationalPanels(state, pendingAction),
|
||||
openSpatialScene() {},
|
||||
activateAutomaticSpatialSource() {},
|
||||
sourceLabel: "Ожидание",
|
||||
@@ -2872,7 +2877,7 @@ test("runtime errors borrow diagnostics only from their exact Connect attempt",
|
||||
);
|
||||
});
|
||||
|
||||
test("connection recovery observation follows recommended exact policy then safe priority", () => {
|
||||
test("connection recovery keeps current then durable priority over a stale fresh recommendation", () => {
|
||||
const state = durableTopologyState();
|
||||
state.snapshot_runtime_id = "runtime-recovery-priority";
|
||||
state.devices = [{
|
||||
@@ -2893,7 +2898,7 @@ test("connection recovery observation follows recommended exact policy then safe
|
||||
state.connection_policy = {
|
||||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||||
facts: { retained_context_is_presence: false },
|
||||
recommended_action: "observe-configured-device-network",
|
||||
recommended_action: "observe-fresh-device-network",
|
||||
allowed_actions: [
|
||||
"observe-current-device-network",
|
||||
"observe-configured-device-network",
|
||||
@@ -2913,10 +2918,21 @@ test("connection recovery observation follows recommended exact policy then safe
|
||||
};
|
||||
|
||||
const recommended = recommendedConnectionRecoveryObservationTarget(state);
|
||||
assert.equal(recommended?.action, "observe-configured-device-network");
|
||||
assert.equal(recommended?.deviceId, "configured-k1");
|
||||
assert.equal(recommended?.action, "observe-current-device-network");
|
||||
assert.equal(recommended?.deviceId, "current-k1");
|
||||
|
||||
state.connection_policy.actions["observe-current-device-network"].allowed = false;
|
||||
const durable = recommendedConnectionRecoveryObservationTarget(state);
|
||||
assert.equal(durable?.action, "observe-configured-device-network");
|
||||
assert.equal(durable?.deviceId, "configured-k1");
|
||||
|
||||
state.connection_policy.actions["observe-configured-device-network"].allowed = false;
|
||||
const fresh = recommendedConnectionRecoveryObservationTarget(state);
|
||||
assert.equal(fresh?.action, "observe-fresh-device-network");
|
||||
assert.equal(fresh?.deviceId, "fresh-k1");
|
||||
|
||||
state.connection_policy.recommended_action = "scan-ble";
|
||||
state.connection_policy.actions["observe-current-device-network"].allowed = true;
|
||||
const fallback = recommendedConnectionRecoveryObservationTarget(state);
|
||||
assert.equal(fallback?.action, "observe-current-device-network");
|
||||
assert.equal(fallback?.deviceId, "current-k1");
|
||||
@@ -3109,6 +3125,19 @@ test("cold disconnected connection SSR hides every operational panel", () => {
|
||||
assert.doesNotMatch(unresolvedHistoryMarkup, /class="device-workspace__side/);
|
||||
});
|
||||
|
||||
test("live preparation keeps operational panels mounted across a control handoff", () => {
|
||||
const state = durableTopologyState();
|
||||
const markup = renderConnectionPipelines(state, "live");
|
||||
|
||||
assert.equal(shouldRenderK1OperationalPanels(state, "live"), true);
|
||||
assert.match(markup, /class="metrics-grid/);
|
||||
assert.match(markup, /class="[^"]*\bsession-panel\b/);
|
||||
assert.match(markup, /class="diagnostics-grid/);
|
||||
assert.match(markup, /class="device-workspace__side/);
|
||||
assert.match(markup, /Подключение установлено/);
|
||||
assert.doesNotMatch(markup, /Переподключиться|Подключить заново/);
|
||||
});
|
||||
|
||||
test("connected, active, replay and recovery SSR retain operational panels", () => {
|
||||
const connected = runtimeState();
|
||||
connected.phase = "connected";
|
||||
@@ -3911,6 +3940,34 @@ test("explicit read-only recovery keeps its network-unavailable classification",
|
||||
}
|
||||
});
|
||||
|
||||
test("a BLE-proven missing Bridge address requires network setup across later retries", () => {
|
||||
const state = {
|
||||
connection_verification: {
|
||||
status: "unreachable",
|
||||
reason_code: "connection-verify-address-unavailable",
|
||||
},
|
||||
last_operation: {
|
||||
action: "connection.verify",
|
||||
status: "failed",
|
||||
error: { code: "connection-verify-device-not-rediscovered" },
|
||||
},
|
||||
};
|
||||
assert.equal(savedBridgeRequiresNetworkSetup(state), true);
|
||||
assert.equal(savedBridgeRequiresNetworkSetup({
|
||||
last_operation: {
|
||||
action: "connection.verify",
|
||||
status: "failed",
|
||||
error: { code: "connection-verify-address-unavailable" },
|
||||
},
|
||||
}), true);
|
||||
assert.equal(savedBridgeRequiresNetworkSetup({
|
||||
connection_verification: {
|
||||
status: "unreachable",
|
||||
reason_code: "connection-verify-device-not-rediscovered",
|
||||
},
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("pre-write candidate loss is classified stale without granting UI continuation", () => {
|
||||
for (const reasonCode of [
|
||||
"BleakDeviceNotFoundError",
|
||||
@@ -4256,7 +4313,7 @@ test("fresh Scan keeps retired audit rows selectable without recovery I/O", () =
|
||||
assertCanonicalConnectionCopy(historicalAuditMarkup);
|
||||
});
|
||||
|
||||
test("only current network recovery safe-next states retain a historical applied attempt", () => {
|
||||
test("only an active network operation owns an applied-attempt screen", () => {
|
||||
const attempt = {
|
||||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||||
attempt_id: "attempt-network-recovery-ownership",
|
||||
@@ -4283,7 +4340,7 @@ test("only current network recovery safe-next states retain a historical applied
|
||||
...attempt,
|
||||
safe_next_action: safeNextAction,
|
||||
}),
|
||||
true,
|
||||
false,
|
||||
safeNextAction,
|
||||
);
|
||||
}
|
||||
@@ -5205,6 +5262,51 @@ test("an unresolved physical command has one explicit server-bound read-only rec
|
||||
/Результаты последнего Bluetooth-поиска|nearby-unrelated-device|>Выбрать<|>Применить</,
|
||||
);
|
||||
|
||||
const networkSetupState = structuredClone(state);
|
||||
networkSetupState.connection_verification = {
|
||||
status: "unreachable",
|
||||
lease_state: "disconnected",
|
||||
network_reachability: "unreachable",
|
||||
reason_code: "connection-verify-address-unavailable",
|
||||
};
|
||||
networkSetupState.last_operation = {
|
||||
action: "connection.verify",
|
||||
status: "failed",
|
||||
error: { code: "connection-verify-device-not-rediscovered" },
|
||||
};
|
||||
const networkSetupMarkup = renderToStaticMarkup(createElement(
|
||||
K1ProvisioningPipeline,
|
||||
{
|
||||
controller: provisioningController(networkSetupState),
|
||||
desiredMode: "bridge",
|
||||
},
|
||||
));
|
||||
const networkSetupReconnect = buttonMarkupWithText(
|
||||
networkSetupMarkup,
|
||||
"Переподключиться",
|
||||
);
|
||||
assert.equal(networkSetupReconnect.length, 1);
|
||||
assert.doesNotMatch(networkSetupReconnect[0], /\bdisabled(?:=|\s|>)/);
|
||||
const networkSetupAction = buttonMarkupWithText(
|
||||
networkSetupMarkup,
|
||||
"Подключить K1 к общей сети",
|
||||
);
|
||||
assert.equal(networkSetupAction.length, 1);
|
||||
assert.doesNotMatch(networkSetupAction[0], /\bdisabled(?:=|\s|>)/);
|
||||
assert.match(
|
||||
networkSetupMarkup,
|
||||
/K1 не подключён к сохранённой общей сети/,
|
||||
);
|
||||
assert.match(networkSetupMarkup, /не сообщил адрес Bridge/);
|
||||
assert.doesNotMatch(
|
||||
networkSetupMarkup,
|
||||
/Локальная операция завершилась ошибкой|Проверьте питание и сеть/,
|
||||
);
|
||||
assert.match(
|
||||
physicalRecoveryConnectionDetail(networkSetupState),
|
||||
/K1 ответил по Bluetooth, но не подключён к сохранённой общей сети/,
|
||||
);
|
||||
|
||||
const policyDeniedState = structuredClone(state);
|
||||
policyDeniedState.connection_policy.allowed_actions = ["scan-ble"];
|
||||
delete policyDeniedState.connection_policy.actions["observe-configured-device-network"];
|
||||
@@ -5258,9 +5360,10 @@ test("an unresolved physical command has one explicit server-bound read-only rec
|
||||
/scanWithResult\(|connect\(|retireUnavailable|reopenRetired|reconcilePhysicalCommand/,
|
||||
);
|
||||
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
|
||||
assert.match(physicalRecovery, /surfaceErrors: false/);
|
||||
});
|
||||
|
||||
test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible", () => {
|
||||
test("supervisor DeviceInfo Verify outranks a stale exact fresh row for physical recovery", () => {
|
||||
const state = durableTopologyState();
|
||||
const recoveryTransport = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||||
state.snapshot_runtime_id = "runtime-terminal-control-physical-recovery";
|
||||
@@ -5299,11 +5402,20 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
|
||||
recommended_action: "scan-ble",
|
||||
allowed_actions: [
|
||||
"scan-ble",
|
||||
"observe-fresh-device-network",
|
||||
"inspect-configured-endpoint",
|
||||
"verify-control-device-info",
|
||||
],
|
||||
actions: {
|
||||
"observe-fresh-device-network": deniedObservation("fresh-scan"),
|
||||
"observe-fresh-device-network": {
|
||||
allowed: true,
|
||||
reason_codes: [],
|
||||
target_source: "fresh-scan",
|
||||
required_transport_ref: recoveryTransport,
|
||||
required_connection_mode: "bridge",
|
||||
requires_live_gatt_validation: true,
|
||||
automatic_retry: false,
|
||||
},
|
||||
"observe-current-device-network": deniedObservation(
|
||||
"retained-current-process",
|
||||
),
|
||||
@@ -5320,6 +5432,13 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
|
||||
},
|
||||
},
|
||||
};
|
||||
state.devices = [{
|
||||
device_id: recoveryTransport,
|
||||
name: "Projected K1",
|
||||
connectable: true,
|
||||
likely_k1: true,
|
||||
}];
|
||||
state.ble_discovery_generation = 2;
|
||||
|
||||
assert.deepEqual(readOnlyConnectionObservationTarget(state), {
|
||||
action: "verify-control-device-info",
|
||||
@@ -5341,6 +5460,7 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
|
||||
|
||||
const mismatched = structuredClone(state);
|
||||
mismatched.semantic_topology_store.record.transport_ref = "another-k1";
|
||||
mismatched.connection_policy.actions["observe-fresh-device-network"].allowed = false;
|
||||
assert.equal(readOnlyConnectionObservationTarget(mismatched), null);
|
||||
});
|
||||
|
||||
@@ -5929,8 +6049,16 @@ test("terminal Apply recovery is bounded and never claims that the network is re
|
||||
controller: provisioningController(state),
|
||||
desiredMode: "bridge",
|
||||
}, presentation);
|
||||
assert.match(markup, /Управление не подтверждено/);
|
||||
assert.match(markup, /Попытка настройки завершена/);
|
||||
assert.match(markup, /Сохранённое подключение/);
|
||||
assert.match(markup, /Подключить заново/);
|
||||
assert.equal(
|
||||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||||
1,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
markup,
|
||||
/Управление не подтверждено|выбор заблокирован|Попытка настройки завершена/,
|
||||
);
|
||||
assert.doesNotMatch(markup, /Сеть (?:готова|настроена)|Сетевые настройки уже применены/);
|
||||
assert.doesNotMatch(markup, /Пароль передан|FIELD-NET|>Применить</);
|
||||
assert.doesNotMatch(markup, /aria-busy="true"/);
|
||||
@@ -6231,7 +6359,7 @@ test("unknown and cold durable recovery offer one reconnect and one new-device p
|
||||
desiredMode: "bridge",
|
||||
}, presentation);
|
||||
assert.doesNotMatch(unrelatedMarkup, /op-b7ea404d/);
|
||||
assert.match(unrelatedMarkup, /Результат применения сети не подтверждён/);
|
||||
assert.match(unrelatedMarkup, /Сохранённое подключение/);
|
||||
});
|
||||
|
||||
test("read-only reconnect keeps its card and loader across a transient projection", async () => {
|
||||
@@ -6294,7 +6422,7 @@ test("read-only reconnect keeps its card and loader across a transient projectio
|
||||
assert.match(pendingReconnect[0], /disabled/);
|
||||
assert.match(pendingReconnect[0], /aria-busy="true"/);
|
||||
assert.match(pendingReconnect[0], /nodedc-activity-indicator/);
|
||||
assert.match(pendingMarkup, /Сохранённое подключение требует проверки/);
|
||||
assert.match(pendingMarkup, /Сохранённое подключение/);
|
||||
assert.doesNotMatch(pendingMarkup, /connection-action-progress/);
|
||||
assert.equal(
|
||||
harness.stateValue(emptyReadOnlyReconnectPresentation)?.kind,
|
||||
@@ -6320,7 +6448,7 @@ test("read-only reconnect keeps its card and loader across a transient projectio
|
||||
}
|
||||
});
|
||||
|
||||
test("cold saved new-device path resets once, performs zero Scan, and survives reload", async () => {
|
||||
test("cold saved target keeps fast reconnect after a reset and reload", async () => {
|
||||
const state = terminalConnectionRecoveryState();
|
||||
state.connection_attempt = null;
|
||||
const target = recommendedConnectionRecoveryObservationTarget(state);
|
||||
@@ -6424,14 +6552,18 @@ test("cold saved new-device path resets once, performs zero Scan, and survives r
|
||||
},
|
||||
desiredMode: "bridge",
|
||||
});
|
||||
assert.equal(buttonMarkupWithText(reloadedMarkup, "Переподключиться").length, 0);
|
||||
const reloadedReconnect = buttonMarkupWithText(
|
||||
reloadedMarkup,
|
||||
"Переподключиться",
|
||||
);
|
||||
assert.equal(reloadedReconnect.length, 1);
|
||||
assert.doesNotMatch(reloadedReconnect[0], /\bdisabled(?:=|\s|>)/);
|
||||
const cleanScan = buttonMarkupWithText(reloadedMarkup, "Найти по Bluetooth");
|
||||
assert.equal(cleanScan.length, 1);
|
||||
assert.doesNotMatch(cleanScan[0], /\bdisabled(?:=|\s|>)/);
|
||||
assert.doesNotMatch(
|
||||
reloadedMarkup,
|
||||
/Нужна проверка|Прежнее подключение не подтверждено|Проверяем прежний K1/,
|
||||
);
|
||||
assert.match(reloadedMarkup, /Сохранённое подключение/);
|
||||
assert.match(reloadedMarkup, /Можно переподключиться/);
|
||||
assert.doesNotMatch(reloadedMarkup, /Подключить новый K1/);
|
||||
});
|
||||
|
||||
test("a current post-reset failed or unknown attempt outranks the durable reset marker after reload", () => {
|
||||
@@ -6478,7 +6610,7 @@ test("a current post-reset failed or unknown attempt outranks the durable reset
|
||||
},
|
||||
desiredMode: "bridge",
|
||||
});
|
||||
assert.match(unknownReloadMarkup, /Результат применения сети не подтверждён/);
|
||||
assert.match(unknownReloadMarkup, /Сохранённое подключение/);
|
||||
assert.equal(
|
||||
buttonMarkupWithText(unknownReloadMarkup, "Переподключиться").length,
|
||||
1,
|
||||
@@ -6501,9 +6633,20 @@ test("a current post-reset failed or unknown attempt outranks the durable reset
|
||||
controller: provisioningController(failed),
|
||||
desiredMode: "bridge",
|
||||
});
|
||||
assert.match(failedReloadMarkup, /Управление не подтверждено/);
|
||||
assert.match(failedReloadMarkup, /Новый выбор временно заблокирован/);
|
||||
assert.doesNotMatch(failedReloadMarkup, /Совпадений нет|Ожидает/);
|
||||
assert.match(failedReloadMarkup, /Сохранённое подключение/);
|
||||
assert.match(failedReloadMarkup, /Можно переподключиться/);
|
||||
assert.equal(
|
||||
buttonMarkupWithText(failedReloadMarkup, "Переподключиться").length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
buttonMarkupWithText(failedReloadMarkup, "Подключить новый K1").length,
|
||||
1,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
failedReloadMarkup,
|
||||
/Управление не подтверждено|выбор заблокирован|Новый выбор временно заблокирован/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Apply never owns hidden refresh or an automatic continuation", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ let claimExclusiveLiveViewer;
|
||||
let createRecordedOpenWatchdog;
|
||||
let createReentrantViewerDisposer;
|
||||
let isLiveRerunPresentationReady;
|
||||
let liveTimelineNeedsSynchronization;
|
||||
let liveRerunReceiverBindingIdentity;
|
||||
let recordedOpenWatchdogTimeoutMs;
|
||||
let rerunViewerInitialSource;
|
||||
@@ -26,6 +27,7 @@ before(async () => {
|
||||
createRecordedOpenWatchdog,
|
||||
createReentrantViewerDisposer,
|
||||
isLiveRerunPresentationReady,
|
||||
liveTimelineNeedsSynchronization,
|
||||
liveRerunReceiverBindingIdentity,
|
||||
recordedOpenWatchdogTimeoutMs,
|
||||
rerunViewerInitialSource,
|
||||
@@ -82,6 +84,14 @@ test("live presentation waits for the exact receiver to expose a usable range",
|
||||
assert.equal(isLiveRerunPresentationReady(true, { min: 1, max: 1 }, 1), true);
|
||||
});
|
||||
|
||||
test("live timeline is retried only after stream_time exists and until it is active", () => {
|
||||
assert.equal(liveTimelineNeedsSynchronization(false, undefined, { min: 1, max: 2 }), false);
|
||||
assert.equal(liveTimelineNeedsSynchronization(true, undefined, null), false);
|
||||
assert.equal(liveTimelineNeedsSynchronization(true, undefined, { min: 1, max: 2 }), true);
|
||||
assert.equal(liveTimelineNeedsSynchronization(true, "log_time", { min: 1, max: 2 }), true);
|
||||
assert.equal(liveTimelineNeedsSynchronization(true, "stream_time", { min: 1, max: 2 }), false);
|
||||
});
|
||||
|
||||
test("live receiver binding stays stable across recovery authority projections", () => {
|
||||
const sourceUrl = "rerun+http://127.0.0.1:9877/proxy";
|
||||
const streamId = "acq-001";
|
||||
@@ -217,7 +227,11 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const readyToRender = followLive\s*\? isLiveRerunPresentationReady\(/,
|
||||
/const readyToRender = followLive\s*\? liveTimelineSynchronized && isLiveRerunPresentationReady\(/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/viewer\.get_active_timeline\(event\.recording_id\)[\s\S]*viewer\.set_active_timeline\(event\.recording_id, timeline\)/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
@@ -257,7 +271,16 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
|
||||
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(source, /followLive=\{!recordedReplay && streamActive\}/);
|
||||
assert.match(
|
||||
source,
|
||||
/const liveRerunSource = !recordedSource && \/\^rerun\\\+https\?:\\\/\\\//,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const livePresentationActivitySequence = metrics\?\.publishedFrameCount \?\?[\s\S]*liveRerunSource && !streamActive \? 1 : null/,
|
||||
);
|
||||
assert.match(source, /followLive=\{liveRerunSource\}/);
|
||||
assert.match(source, /liveActivitySequence=\{livePresentationActivitySequence\}/);
|
||||
assert.match(
|
||||
source,
|
||||
/sourceUrl\.trim\(\) && pointCloudVisible && !intentionalSourceEnd/,
|
||||
|
||||
Reference in New Issue
Block a user