Защита Bridge и камеры K1 при переподключении

This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 01:25:25 +03:00
parent 0752e5c6bf
commit 71c8b043c9
8 changed files with 737 additions and 87 deletions
@@ -96,6 +96,23 @@ export function livePresentationCloseFence(
return acquisitionId ? JSON.stringify([source.id, acquisitionId]) : null;
}
function acquisitionIdFromLivePresentationCloseFence(fence: string): string | null {
try {
const decoded: unknown = JSON.parse(fence);
if (
!Array.isArray(decoded)
|| decoded.length !== 2
|| typeof decoded[0] !== "string"
|| typeof decoded[1] !== "string"
|| !decoded[0].trim()
|| !decoded[1].trim()
) return null;
return decoded[1].trim();
} catch {
return null;
}
}
export interface LiveDefaultPresentationAdmission {
visibleIds: string[];
removedIds: string[];
@@ -132,9 +149,11 @@ export function admitLiveDefaultPresentations(
/**
* A restored workspace may hide cameras that the device plugin has not selected.
* It must still admit the exact selected delivery in a fresh browser document,
* or a replacement delivery for a camera already presented in this acquisition.
* a replacement delivery for a camera already presented in this acquisition,
* or the first delivery of a later acquisition in the same browser document.
* An explicit close remains a separate acquisition-scoped fence in
* `admitLiveDefaultPresentations` and de-selects the source in the plugin first.
* `admitLiveDefaultPresentations`; whether it may de-select the source remains
* the plugin's current activation-authority decision.
*/
export function restoredLayoutMayAdmitLiveDefault(
sources: readonly ObservationSourceDescriptor[],
@@ -145,7 +164,13 @@ export function restoredLayoutMayAdmitLiveDefault(
if (presentedAcquisitionSources.size === 0) return true;
return selectedSources.some((source) => {
const lineage = livePresentationCloseFence(source);
return Boolean(lineage && presentedAcquisitionSources.has(lineage));
if (!lineage) return false;
if (presentedAcquisitionSources.has(lineage)) return true;
const acquisitionId = source.binding.acquisitionId?.trim();
if (!acquisitionId) return false;
return ![...presentedAcquisitionSources].some(
(presented) => acquisitionIdFromLivePresentationCloseFence(presented) === acquisitionId,
);
});
}
@@ -5355,6 +5355,55 @@ test("an unresolved physical command has one explicit server-bound read-only rec
/Результаты последнего Bluetooth-поиска|nearby-unrelated-device|>Выбрать<|>Применить</,
);
const quickRecoveryInsideBridgeState = structuredClone(state);
quickRecoveryInsideBridgeState.physical_command.record.connection.connection_mode =
"quick-connect";
quickRecoveryInsideBridgeState.physical_command.record.connection.target_ipv4 =
"192.168.56.1";
quickRecoveryInsideBridgeState.semantic_topology_store.record.connection_mode =
"quick-connect";
quickRecoveryInsideBridgeState.semantic_topology_store.record.ipv4 = "192.168.56.1";
quickRecoveryInsideBridgeState.connection_policy.actions[
"observe-configured-device-network"
].required_connection_mode = "quick-connect";
const quickRecoveryInsideBridgeMarkup = renderToStaticMarkup(createElement(
K1ProvisioningPipeline,
{
controller: provisioningController(quickRecoveryInsideBridgeState),
desiredMode: "bridge",
},
));
assert.equal(
buttonMarkupWithText(
quickRecoveryInsideBridgeMarkup,
"Переподключиться",
).length,
0,
);
assert.equal(
buttonMarkupWithText(
quickRecoveryInsideBridgeMarkup,
"Подключить новый K1",
).length,
1,
);
const selectedQuickRecoveryState = structuredClone(
quickRecoveryInsideBridgeState,
);
selectedQuickRecoveryState.desired_connection_mode = "quick-connect";
const selectedQuickRecoveryMarkup = renderToStaticMarkup(createElement(
K1ProvisioningPipeline,
{
controller: provisioningController(selectedQuickRecoveryState),
desiredMode: "quick-connect",
},
));
assert.equal(
buttonMarkupWithText(selectedQuickRecoveryMarkup, "Переподключиться").length,
1,
);
const networkSetupState = structuredClone(state);
networkSetupState.connection_verification = {
status: "unreachable",
@@ -5454,6 +5503,14 @@ test("an unresolved physical command has one explicit server-bound read-only rec
);
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
assert.match(physicalRecovery, /surfaceErrors: false/);
assert.match(
physicalRecovery,
/physicalRecoveryTarget\.connectionMode !== connectionMode/,
);
assert.doesNotMatch(
physicalRecovery,
/commitDesiredModeForExplicitAction|selectConnectionMode\(/,
);
});
test("supervisor DeviceInfo Verify outranks a stale exact fresh row for physical recovery", () => {
@@ -310,7 +310,7 @@ 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", () => {
test("a restored layout admits same-lineage replacements and the next acquisition", () => {
const camera = (acquisitionId, deliveryId) => ({
id: "k1:sensor.camera.right",
sourceId: "sensor.camera.right",
@@ -344,7 +344,11 @@ test("a restored layout admits a selected delivery after reload and only same-ac
assert.equal(restoredLayoutMayAdmitLiveDefault([original], new Set()), true);
assert.equal(restoredLayoutMayAdmitLiveDefault([successor], presented), true);
assert.equal(restoredLayoutMayAdmitLiveDefault([unrelated], presented), false);
assert.equal(
restoredLayoutMayAdmitLiveDefault([unrelated], presented),
true,
"Bridge acquisition A must not suppress the first Quick Connect camera in acquisition B",
);
assert.equal(restoredLayoutMayAdmitLiveDefault([{
...original,
activation: { ...original.activation, selected: false },
+1
View File
@@ -1139,6 +1139,7 @@ interface ConnectionVerifyRequestBase {
device_id: string;
compatibility_attestation: CompatibilityAttestation;
operation_id?: string;
expected_mode_revision: number;
}
export type ConnectionVerifyRequest =
@@ -819,11 +819,16 @@ export function localProvisioningDraftFenceKey({
function observationRequest(
target: ReadOnlyConnectionObservationTarget,
desiredModeRevision: number | null | undefined,
reconfigurationRevision: number,
reconfigurationIntentId: string | null,
): ConnectionVerifyRequest | null {
if (!Number.isInteger(desiredModeRevision) || (desiredModeRevision ?? -1) < 0) {
return null;
}
const commonRequest = {
device_id: target.deviceId,
expected_mode_revision: desiredModeRevision as number,
compatibility_attestation: profileSelectionForConnectionMode(
target.connectionMode,
),
@@ -1688,7 +1693,8 @@ export function K1ProvisioningPipeline({
currentReadOnlyReconnectPresentation?.kind === "connection"
&& currentReadOnlyReconnectPresentation.connectionMode === connectionMode;
const physicalRecoveryVerificationPending =
currentReadOnlyReconnectPresentation?.kind === "physical";
currentReadOnlyReconnectPresentation?.kind === "physical"
&& currentReadOnlyReconnectPresentation.connectionMode === connectionMode;
const provisioningMutationBusy = Boolean(
livePreparationPending
|| presentedPendingAction
@@ -1886,8 +1892,15 @@ export function K1ProvisioningPipeline({
const physicalRecoveryAuthorityKey = physicalRecoveryPresentationAuthorityKey(
state,
);
const physicalRecoveryMatchesSelectedMode = Boolean(
physicalRecoveryTarget?.connectionMode === connectionMode
&& state?.desired_connection_mode === connectionMode,
);
const physicalReadOnlyVerificationAvailable = Boolean(
physicalRecoveryTarget?.serverBound || physicalRecoveryVerificationPending,
(
physicalRecoveryMatchesSelectedMode
&& physicalRecoveryTarget?.serverBound
) || physicalRecoveryVerificationPending,
);
const presentedPhysicalRecoveryMode = physicalRecoveryBinding?.connectionMode
?? (physicalRecoveryVerificationPending
@@ -2588,6 +2601,7 @@ export function K1ProvisioningPipeline({
const request = appliedNetworkRecoveryTarget
? observationRequest(
appliedNetworkRecoveryTarget,
desiredModeRevision,
reconfigurationRevision,
reconfigurationIntentId,
)
@@ -2644,6 +2658,7 @@ export function K1ProvisioningPipeline({
async (target) => {
const request = observationRequest(
target,
desiredModeRevision,
reconfigurationRevision,
reconfigurationIntentId,
);
@@ -2709,12 +2724,23 @@ export function K1ProvisioningPipeline({
if (
!physicalRecoveryRequired
|| !physicalRecoveryTarget?.serverBound
|| physicalRecoveryTarget.connectionMode !== connectionMode
|| state?.desired_connection_mode !== connectionMode
|| !snapshotRuntimeId
|| !verificationKey
|| isBusy
|| typeof getConnectionActionAuthority !== "function"
) return;
const modeAuthority = getConnectionActionAuthority(connectionMode);
if (!modeAuthority) {
setCandidateUnavailableMessage(
"Не удалось переподключиться: состояние K1 изменилось. Повторите проверку или подключите новый K1 вручную.",
);
return;
}
const request = observationRequest(
physicalRecoveryTarget,
modeAuthority.desiredModeRevision,
reconfigurationRevision,
reconfigurationIntentId,
);
@@ -2734,19 +2760,10 @@ export function K1ProvisioningPipeline({
});
setCandidateUnavailableMessage(null);
try {
const modeAuthority = await commitDesiredModeForExplicitAction(
physicalRecoveryTarget.connectionMode,
);
if (!localScenarioActionEpochIsCurrent(
actionEpoch,
localScenarioActionEpoch.current,
)) return;
if (!modeAuthority) {
setCandidateUnavailableMessage(
"Не удалось переподключиться: состояние K1 изменилось. Повторите проверку или подключите новый K1 вручную.",
);
return;
}
const actionFence = activateRuntimeActionFence(modeAuthority);
if (!actionFence || !runtimeActionIsCurrent(actionFence)) return;
try {
@@ -120,6 +120,18 @@ PYTEST_TARGETS = (
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_to_bridge_does_not_retrieve_old_handle_without_new_scan"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_verify_is_rejected_while_bridge_is_selected_before_any_io"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_physical_recovery_cannot_silently_change_bridge_draft_to_quick"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_public_quick_physical_recovery_rejoins_saved_wifi_after_live_ble_proof"
),
)
+296 -8
View File
@@ -1076,6 +1076,10 @@ class ConnectionVerifyRequest(StrictRequest):
)
compatibility_attestation: CompatibilityAttestationRequest | None = None
operation_id: str | None = Field(default=None, min_length=1, max_length=128)
# New clients bind Verify to the exact mode draft rendered at the click.
# Optionality keeps the legacy server-resolved `{}` request and older
# local clients readable; mode equality remains mandatory either way.
expected_mode_revision: int | None = Field(default=None, ge=0)
expected_discovery_generation: int | None = Field(default=None, ge=0)
expected_reconfiguration_revision: int | None = Field(default=None, ge=0)
expected_reconfiguration_intent_id: str | None = Field(
@@ -8236,7 +8240,10 @@ class XgridsK1CompatibilityService:
active_profile_id,
device_session_id,
camera_preview,
activation_admitted=camera_activation_admitted,
# Automatic acquisition camera ownership is deliberately not
# manual preview authority. The selected delivery remains
# visible, but source controls cannot detach evidence.
activation_admitted=(camera_activation_admitted and not acquisition_active),
),
"device_calibration": device_calibration,
"camera_preview": camera_preview,
@@ -9400,6 +9407,22 @@ class XgridsK1CompatibilityService:
runtime = self.runtime.snapshot()
control = self._application_control_session.snapshot()
physical = self._physical_command_coordinator.snapshot()
physical_recovery_requires_explicit_reset = bool(
physical.get("resolved_unclassified_stop_recovery_required")
is True
)
if physical_recovery_requires_explicit_reset:
# A recovery card is bound to the mode recorded by the
# physical ledger. An old browser must never turn its
# action into a hidden mode change; only the visible
# scenario-reset control may retire that ownership.
raise NetworkProvisioningConflict(
"незавершённое физическое состояние K1 требует явного "
"сброса сценария перед сменой способа подключения",
reason_code=(
"connection-mode-selection-physical-recovery-reset-required"
),
)
acquisition_state = acquisition.state if acquisition is not None else None
selection_reasons = _connection_mode_selection_reason_codes(
acquisition_state=acquisition_state,
@@ -9694,6 +9717,39 @@ class XgridsK1CompatibilityService:
reason_code="network-provision-discovery-generation-conflict",
)
def _require_connection_mode_draft_for_verify(
self,
requested_mode: ConnectionMode | None,
*,
expected_mode_revision: int | None,
) -> None:
"""Fence Verify before BLE, host Wi-Fi, MQTT or topology mutation."""
with self._lock:
desired = self._desired_connection_mode
revision = self._desired_connection_mode_revision
scenario_reset_pending = self._connection_scenario_reset_pending is not None
if scenario_reset_pending:
raise ConnectionVerificationError(
"сначала завершается явная смена сценария подключения",
reason_code="connection-mode-switch-pending",
)
if requested_mode is None:
raise ConnectionVerificationError(
"Read-only проверка K1 не получила точный способ подключения",
reason_code="connection-verify-connection-missing",
)
if expected_mode_revision is not None and expected_mode_revision != revision:
raise ConnectionVerificationError(
"способ подключения изменился до начала проверки",
reason_code="connection-mode-draft-revision-conflict",
)
if requested_mode != desired:
raise ConnectionVerificationError(
"проверяемое подключение не совпадает с выбранным способом",
reason_code="connection-mode-draft-mismatch",
)
def _require_current_connection_reconfiguration_target(
self,
*,
@@ -15700,6 +15756,10 @@ class XgridsK1CompatibilityService:
else None
)
try:
self._require_connection_mode_draft_for_verify(
cast(ConnectionMode | None, requested_mode),
expected_mode_revision=request.expected_mode_revision,
)
self._require_current_connection_reconfiguration_target(
expected_revision=request.expected_reconfiguration_revision,
expected_intent_id=request.expected_reconfiguration_intent_id,
@@ -15746,6 +15806,10 @@ class XgridsK1CompatibilityService:
"фоновая проверка подключения не завершилась вовремя",
reason_code="connection-verify-lifecycle-busy",
)
self._require_connection_mode_draft_for_verify(
cast(ConnectionMode | None, requested_mode),
expected_mode_revision=request.expected_mode_revision,
)
self._require_current_connection_reconfiguration_target(
expected_revision=request.expected_reconfiguration_revision,
expected_intent_id=request.expected_reconfiguration_intent_id,
@@ -17944,6 +18008,7 @@ class XgridsK1CompatibilityService:
if isinstance(item.get("device_id"), str) and str(item.get("device_id")).strip()
}
discovery_generation = self._ble_discovery_generation
desired_mode_revision = self._desired_connection_mode_revision
selected_device_id = self._selected_device_id
selected_connection_mode = self._connection_mode
selected_device_session_id = self._device_session_id
@@ -18026,6 +18091,11 @@ class XgridsK1CompatibilityService:
verification="live-device-info",
),
operation_id=request.operation_id,
expected_mode_revision=(
request.expected_mode_revision
if request.expected_mode_revision is not None
else desired_mode_revision
),
expected_discovery_generation=(
discovery_generation if source == "fresh-scan" else None
),
@@ -18054,7 +18124,11 @@ class XgridsK1CompatibilityService:
"direct-lan": "bridge",
"device-ap": "quick-connect",
"controller-hotspot": "direct-connect",
}[request.compatibility_attestation.topology],
}[request.compatibility_attestation.topology],
)
self._require_connection_mode_draft_for_verify(
requested_mode,
expected_mode_revision=request.expected_mode_revision,
)
self._require_current_connection_reconfiguration_target(
expected_revision=request.expected_reconfiguration_revision,
@@ -18085,6 +18159,7 @@ class XgridsK1CompatibilityService:
{
"device_id": request.device_id,
"source": request.source,
"expected_mode_revision": request.expected_mode_revision,
"expected_discovery_generation": (request.expected_discovery_generation),
"expected_reconfiguration_revision": (request.expected_reconfiguration_revision),
"expected_reconfiguration_intent_id": (request.expected_reconfiguration_intent_id),
@@ -18222,6 +18297,7 @@ class XgridsK1CompatibilityService:
request.compatibility_attestation,
verification_source=request.source,
expected_discovery_generation=(request.expected_discovery_generation),
expected_mode_revision=request.expected_mode_revision,
require_live_gatt_validation=(physical_recovery_target is not None),
)
except asyncio.CancelledError:
@@ -18339,9 +18415,14 @@ class XgridsK1CompatibilityService:
*,
transport_ref: str,
target_ipv4: str,
expected_mode_revision: int | None,
) -> _SavedQuickConnectHostAssociation:
"""Restore the controller-side AP route without touching K1 over BLE."""
self._require_connection_mode_draft_for_verify(
"quick-connect",
expected_mode_revision=expected_mode_revision,
)
binding = _recover_durable_quick_connect_host_binding(
self.evidence_root,
transport_ref=transport_ref,
@@ -18401,6 +18482,13 @@ class XgridsK1CompatibilityService:
)
try:
if association_performed:
# Re-sample immediately before the only host-network mutation.
# A queued scenario reset or stale UI revision wins without
# invoking CoreWLAN.
self._require_connection_mode_draft_for_verify(
"quick-connect",
expected_mode_revision=expected_mode_revision,
)
association = await _run_blocking_operation_without_abandonment(
associate_with_wifi_profile_once,
self.repository_root
@@ -18504,6 +18592,109 @@ class XgridsK1CompatibilityService:
evidence_session_dir=session_dir,
)
async def _restore_saved_quick_connect_host_path(
self,
*,
transport_ref: str,
target_ipv4: str,
ble_operation_performed: bool,
expected_mode_revision: int | None,
) -> tuple[
_SavedQuickConnectHostAssociation,
_ConfiguredEndpointHostObservation,
int,
]:
"""Restore one exact saved AP route and prove its sole MQTT endpoint."""
association = await self._associate_saved_quick_connect_host(
transport_ref=transport_ref,
target_ipv4=target_ipv4,
expected_mode_revision=expected_mode_revision,
)
supervisor_epoch_before = self._connection_supervisor.snapshot().host_path.epoch
try:
observation = await _run_blocking_operation_without_abandonment(
_probe_quick_connect_endpoint_after_route_settle,
target_ipv4,
path_probe=lambda target: self._sample_host_path(
target,
association_timeout_seconds=(
CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
),
),
settle_timeout_seconds=(
DURABLE_QUICK_CONNECT_ROUTE_SETTLE_TIMEOUT_SECONDS
),
settle_interval_seconds=(
DURABLE_QUICK_CONNECT_ROUTE_SETTLE_INTERVAL_SECONDS
),
)
except Exception as exc:
_write_quick_connect_host_network_proof(
association,
target_ipv4=target_ipv4,
observation=None,
outcome="probe-failed",
reason_code=(
getattr(exc, "reason_code", None) or "host-network-proof-failed"
),
supervisor_host_path_epoch_before_admission=supervisor_epoch_before,
ble_operation_performed=ble_operation_performed,
)
raise
if observation.reason_code == "host-route-changed-during-tcp-probe":
_write_quick_connect_host_network_proof(
association,
target_ipv4=target_ipv4,
observation=observation,
outcome="route-changed",
reason_code="connection-verify-lease-changed",
supervisor_host_path_epoch_before_admission=supervisor_epoch_before,
ble_operation_performed=ble_operation_performed,
)
raise ConnectionVerificationError(
"Маршрут к K1 изменился во время Quick Connect проверки",
reason_code="connection-verify-lease-changed",
)
if not observation.path.available or observation.path.route_class != "direct":
_write_quick_connect_host_network_proof(
association,
target_ipv4=target_ipv4,
observation=observation,
outcome="route-rejected",
reason_code="connection-verify-route-mismatch",
supervisor_host_path_epoch_before_admission=supervisor_epoch_before,
ble_operation_performed=ble_operation_performed,
)
raise ConnectionVerificationError(
"После подключения Quick Connect прямой маршрут к K1 не подтверждён",
reason_code="connection-verify-route-mismatch",
)
if not observation.reachable:
_write_quick_connect_host_network_proof(
association,
target_ipv4=target_ipv4,
observation=observation,
outcome="endpoint-unreachable",
reason_code="connection-verify-mqtt-unreachable",
supervisor_host_path_epoch_before_admission=supervisor_epoch_before,
ble_operation_performed=ble_operation_performed,
)
raise ConnectionVerificationError(
"Прямой маршрут Quick Connect подтверждён, но MQTT endpoint K1 недоступен",
reason_code="connection-verify-mqtt-unreachable",
)
_write_quick_connect_host_network_proof(
association,
target_ipv4=target_ipv4,
observation=observation,
outcome="direct-endpoint-reachable",
reason_code=None,
supervisor_host_path_epoch_before_admission=supervisor_epoch_before,
ble_operation_performed=ble_operation_performed,
)
return association, observation, supervisor_epoch_before
async def _adopt_existing_lan_connection(
self,
device_id: str,
@@ -18513,6 +18704,7 @@ class XgridsK1CompatibilityService:
"fresh-scan", "retained-current-process", "durable-configured-state"
] = "fresh-scan",
expected_discovery_generation: int | None = None,
expected_mode_revision: int | None = None,
require_live_gatt_validation: bool = False,
) -> tuple[str, _ProvisionalFreshBridgeTopology | None]:
"""Create a process lease from live or persisted device topology evidence."""
@@ -18526,6 +18718,10 @@ class XgridsK1CompatibilityService:
requested_mode = "direct-connect"
else:
raise ValueError("read-only K1 adoption received an unsupported topology")
self._require_connection_mode_draft_for_verify(
requested_mode,
expected_mode_revision=expected_mode_revision,
)
with self._lock:
scanned_device = next(
(
@@ -18877,6 +19073,10 @@ class XgridsK1CompatibilityService:
# Publish the BLE verification fence before awaiting native I/O.
# A scan cannot invalidate a capture or its validation handoff in
# the post-GATT/pre-pin window.
self._require_connection_mode_draft_for_verify(
requested_mode,
expected_mode_revision=expected_mode_revision,
)
with self._lock:
if self._provisioning_active:
raise RuntimeError("настройка Wi-Fi началась во время проверки сети")
@@ -18897,9 +19097,14 @@ class XgridsK1CompatibilityService:
quick_host_association: _SavedQuickConnectHostAssociation | None = None
quick_supervisor_epoch_before: int | None = None
if requested_mode == "quick-connect":
self._require_connection_mode_draft_for_verify(
requested_mode,
expected_mode_revision=expected_mode_revision,
)
quick_host_association = await self._associate_saved_quick_connect_host(
transport_ref=actual_transport_ref,
target_ipv4=semantic_record.ipv4,
expected_mode_revision=expected_mode_revision,
)
quick_supervisor_epoch_before = (
self._connection_supervisor.snapshot().host_path.epoch
@@ -19132,6 +19337,10 @@ class XgridsK1CompatibilityService:
transport_source == "durable-state"
and require_live_gatt_validation
)
self._require_connection_mode_draft_for_verify(
requested_mode,
expected_mode_revision=expected_mode_revision,
)
status_read = await read_wifi_status_once(
actual_transport_ref,
timeout_seconds=20.0,
@@ -19372,6 +19581,38 @@ class XgridsK1CompatibilityService:
)
if retained_after_read["status"] != "retained":
raise RuntimeError("process recovery token изменился после GATT validation")
live_quick_host_association: _SavedQuickConnectHostAssociation | None = None
live_quick_endpoint_observation: _ConfiguredEndpointHostObservation | None = None
live_quick_supervisor_epoch_before: int | None = None
if requested_mode == "quick-connect" and require_live_gatt_validation:
exact_saved_quick_topology = bool(
expected_reconciliation is None
and semantic_record is not None
and physical_transport_ref_comparison_key(semantic_record.transport_ref)
== physical_transport_ref_comparison_key(actual_transport_ref)
and semantic_record.connection_mode == "quick-connect"
and semantic_record.ipv4 == target
and semantic_record.compatibility_profile_id
== XGRIDS_K1_COMPATIBILITY_PROFILE_ID
and semantic_record.firmware_version
== compatibility_attestation.firmware_version
)
if not exact_saved_quick_topology:
raise ConnectionVerificationError(
"Live Quick Connect recovery не имеет точной сохранённой topology "
"этого K1; подключение Wi-Fi Mac не выполнялось",
reason_code="connection-verify-quick-host-binding-unavailable",
)
(
live_quick_host_association,
live_quick_endpoint_observation,
live_quick_supervisor_epoch_before,
) = await self._restore_saved_quick_connect_host_path(
transport_ref=actual_transport_ref,
target_ipv4=target,
ble_operation_performed=True,
expected_mode_revision=expected_mode_revision,
)
defer_fresh_bridge_semantic_commit = bool(
verification_source == "fresh-scan"
and requested_mode == "bridge"
@@ -19538,11 +19779,17 @@ class XgridsK1CompatibilityService:
raise RuntimeError(
"K1 сообщил адрес другой сети; прямой локальный маршрут отсутствует"
)
control_endpoint_observation = await _run_blocking_operation_without_abandonment(
self._probe_control_endpoint,
target,
association_timeout_seconds=COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS,
)
control_endpoint_observation = live_quick_endpoint_observation
if control_endpoint_observation is None:
control_endpoint_observation = (
await _run_blocking_operation_without_abandonment(
self._probe_control_endpoint,
target,
association_timeout_seconds=(
COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS
),
)
)
if not control_endpoint_observation.reachable:
self._update_applied_topology_reachability(
connection_mode=requested_mode,
@@ -19634,6 +19881,21 @@ class XgridsK1CompatibilityService:
path=control_endpoint_observation.path,
reachable=True,
)
if live_quick_host_association is not None:
_write_quick_connect_host_network_proof(
live_quick_host_association,
target_ipv4=target,
observation=control_endpoint_observation,
outcome="direct-endpoint-reachable",
reason_code=None,
supervisor_host_path_epoch_before_admission=(
live_quick_supervisor_epoch_before
),
supervisor_host_path_epoch_after_admission=(
self._connection_supervisor.snapshot().host_path.epoch
),
ble_operation_performed=True,
)
logger.info(
"K1 existing direct-LAN connection adopted without provisioning",
extra={
@@ -22906,6 +23168,7 @@ class XgridsK1CompatibilityService:
def select_camera_preview(self, request: CameraPreviewSelectRequest) -> dict[str, Any]:
camera_holder_acquired = False
try:
self._require_camera_device_session(request.device_session_id)
with self._lock:
current_acquisition = self._acquisition
current_out_dir = self._acquisition_out_dir
@@ -22941,6 +23204,17 @@ class XgridsK1CompatibilityService:
raise RuntimeError(
"camera preview доступен после автоматического запуска правой камеры"
)
if acquisition_active_before_select:
if request.source_id != DEFAULT_ACQUISITION_CAMERA_SOURCE:
raise LocalAcquisitionLifecycleError(
"Во время активного приёма правая камера принадлежит evidence-сессии; "
"переключение видеоканала недоступно до завершения приёма.",
reason_code="acquisition-camera-evidence-owned",
)
# The mandatory right camera is already selected and bound to
# this acquisition. Re-selecting it is a read-only no-op: a UI
# retry must never detach or replace the evidence producer.
return self.state()
camera_holder_acquired = self._ensure_camera_preview_process_lease()
target = self._camera_target_for_session(request.device_session_id)
with self._lock:
@@ -22975,6 +23249,18 @@ class XgridsK1CompatibilityService:
@_serialized_k1_transition_access
def stop_camera_preview(self, request: CameraPreviewStopRequest) -> dict[str, Any]:
self._require_camera_device_session(request.device_session_id)
with self._lock:
acquisition = self._acquisition
acquisition_active = bool(
acquisition is not None
and acquisition.state not in TERMINAL_ACQUISITION_STATES
)
if acquisition_active:
raise LocalAcquisitionLifecycleError(
"Во время активного приёма правая камера принадлежит evidence-сессии; "
"остановите весь приём штатной командой STOP.",
reason_code="acquisition-camera-evidence-owned",
)
self.camera_preview.stop(request.generation)
self._release_camera_preview_process_lease()
return self.state()
@@ -31644,6 +31930,7 @@ def _plugin_execution_http_status(reason_code: object) -> int:
"connection-mode-switch-lifecycle-busy",
"connection-mode-switch-acquisition-changed",
"connection-mode-selection-lifecycle-busy",
"connection-mode-selection-physical-recovery-reset-required",
"connection-mode-selection-physical-state-unsafe",
"connection-mode-selection-control-state-unsafe",
"acquisition-start-lifecycle-busy",
@@ -35098,6 +35385,7 @@ def _write_quick_connect_host_network_proof(
reason_code: str | None,
supervisor_host_path_epoch_before_admission: int | None,
supervisor_host_path_epoch_after_admission: int | None = None,
ble_operation_performed: bool = False,
) -> None:
"""Persist one redacted CoreWLAN -> route -> TCP proof without K1 mutation."""
@@ -35161,7 +35449,7 @@ def _write_quick_connect_host_network_proof(
"supervisor_host_path_epoch_after_admission": (
supervisor_host_path_epoch_after_admission
),
"ble_operation_performed": False,
"ble_operation_performed": ble_operation_performed,
"device_write_performed": False,
"automatic_retry": False,
},
+309 -63
View File
@@ -14120,6 +14120,7 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
assert pre_prepare_fence(lambda: True) is True
events.append(("select", (source_id, target)))
camera_state["active_source_id"] = source_id
camera_state["generation"] = 1
events.append(("record", session_dir))
camera_state["recording"] = {
"active": True,
@@ -14313,9 +14314,23 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
assert any(
stream["source_id"] == "sensor.camera.right"
and stream["activation"]["selected"] is True
and stream["activation"]["controllable"] is True
and stream["activation"]["controllable"] is False
for stream in admitted_camera_streams
)
admitted_generation = admitted_state["camera_preview"]["generation"]
assert isinstance(admitted_generation, int)
with pytest.raises(facade_module.LocalAcquisitionLifecycleError) as stop_error:
service.stop_camera_preview(
facade_module.CameraPreviewStopRequest(
device_session_id=service._device_session_id, # type: ignore[arg-type] # noqa: SLF001
generation=admitted_generation,
)
)
assert stop_error.value.reason_code == "acquisition-camera-evidence-owned"
after_rejected_stop = service.camera_preview.snapshot()
assert after_rejected_stop["active_source_id"] == "sensor.camera.right"
assert after_rejected_stop["recording"]["active"] is True
assert after_rejected_stop["generation"] == admitted_generation
# Every later authoritative PCL for the same lineage is idempotent.
physical_snapshot_calls_before = physical_snapshot_calls
@@ -17570,7 +17585,7 @@ def test_abort_waits_for_start_handoff_then_stops_owned_producers(
assert service._acquisition_session_lease is None # noqa: SLF001
def test_camera_selection_finishes_before_serialized_acquisition_stop(
def test_active_acquisition_camera_selection_is_rejected_before_serialized_stop(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -17591,43 +17606,8 @@ def test_camera_selection_finishes_before_serialized_acquisition_stop(
service._device_session_id = device_session_id # noqa: SLF001
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
entered_camera_arm = threading.Event()
release_camera_arm = threading.Event()
stop_finished = threading.Event()
worker_errors: list[BaseException] = []
events: list[str] = []
def blocked_camera_arm(_out_dir: Path, *, require_session: bool = False) -> None:
assert require_session is True
events.append("arm")
entered_camera_arm.set()
if not release_camera_arm.wait(timeout=2):
raise TimeoutError("test did not release camera arm")
def select_worker() -> None:
try:
service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.left",
device_session_id=device_session_id,
)
)
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
def stop_worker() -> None:
try:
service.stop_acquisition(
_stop_request(
acquisition_id=acquisition_id,
mode="capture-only",
)
)
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
finally:
stop_finished.set()
camera_snapshot = service.camera_preview.snapshot()
camera_snapshot.update(
{
@@ -17647,7 +17627,11 @@ def test_camera_selection_finishes_before_serialized_acquisition_stop(
},
}
)
monkeypatch.setattr(service, "_arm_camera_recording", blocked_camera_arm)
monkeypatch.setattr(
service,
"_arm_camera_recording",
lambda *_args, **_kwargs: events.append("unexpected-arm"),
)
monkeypatch.setattr(
service.camera_preview,
"snapshot",
@@ -17656,13 +17640,7 @@ def test_camera_selection_finishes_before_serialized_acquisition_stop(
monkeypatch.setattr(
service.camera_preview,
"select",
lambda source_id, _target: (
events.append("select")
or {
"active_source_id": source_id,
"recording": {"active": False},
}
),
lambda *_args, **_kwargs: events.append("unexpected-select"),
)
monkeypatch.setattr(
service.camera_preview,
@@ -17670,18 +17648,24 @@ def test_camera_selection_finishes_before_serialized_acquisition_stop(
lambda **_kwargs: events.append("camera-stop"),
)
monkeypatch.setattr(runtime, "stop", lambda: events.append("runtime-stop"))
select_thread = threading.Thread(target=select_worker)
stop_thread = threading.Thread(target=stop_worker)
select_thread.start()
assert entered_camera_arm.wait(timeout=2)
stop_thread.start()
assert not stop_finished.wait(timeout=0.05)
release_camera_arm.set()
select_thread.join(timeout=2)
stop_thread.join(timeout=2)
assert worker_errors == []
assert events == ["arm", "select", "camera-stop", "runtime-stop"]
with pytest.raises(facade_module.LocalAcquisitionLifecycleError) as selection_error:
service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.left",
device_session_id=device_session_id,
)
)
assert selection_error.value.reason_code == "acquisition-camera-evidence-owned"
assert events == []
service.stop_acquisition(
_stop_request(
acquisition_id=acquisition_id,
mode="capture-only",
)
)
assert events == ["camera-stop", "runtime-stop"]
assert service.state()["acquisition"]["state"] == "completed"
assert service._acquisition_session_lease is None # noqa: SLF001
@@ -21646,6 +21630,8 @@ def test_read_only_reconciliation_requires_exact_requested_mode_despite_shared_s
service,
intended_mode=intended_mode,
)
if requested_mode != "bridge":
_select_connection_mode(service, requested_mode)
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
raise AssertionError("mode mismatch must fail before a GATT read")
@@ -21820,6 +21806,13 @@ def test_durable_restart_rejects_request_target_mismatch_before_gatt(
transport_ref=DURABLE_K1_UUID,
)
restarted, _ = service_with_fake_runtime(tmp_path)
requested_mode = {
"direct-lan": "bridge",
"device-ap": "quick-connect",
"controller-hotspot": "direct-connect",
}[attestation.topology]
if requested_mode != "bridge":
_select_connection_mode(restarted, requested_mode)
reads: list[str] = []
async def forbidden_read(*_: object, **__: object) -> dict[str, Any]:
@@ -21989,6 +21982,8 @@ def test_durable_restart_previous_topology_cannot_resolve_ambiguous_write(
previous_connection=previous,
)
restarted, _ = service_with_fake_runtime(tmp_path)
if intended_mode != "bridge":
_select_connection_mode(restarted, intended_mode)
captured = _durable_status_capture()
reads = 0
pin_calls: list[tuple[object, str]] = []
@@ -22123,13 +22118,15 @@ def test_durable_restart_failed_status_keeps_ambiguity_and_does_not_pin(
def _seed_durable_quick_reconnect_evidence(
service: XgridsK1CompatibilityService,
*,
transport_ref: str = DURABLE_K1_UUID,
) -> None:
"""Persist the exact secret-free Quick topology and AP activation proof."""
topology_store = service._semantic_topology_store # noqa: SLF001
assert topology_store is not None
topology_store.commit(
transport_ref=DURABLE_K1_UUID,
transport_ref=transport_ref,
connection_mode="quick-connect",
ipv4="192.168.56.1",
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
@@ -22145,7 +22142,7 @@ def _seed_durable_quick_reconnect_evidence(
json.dumps(
{
"operation": "single_reviewed_quick_connect_ap_activation",
"device_macos_uuid": DURABLE_K1_UUID,
"device_macos_uuid": transport_ref,
"device_name": "XGR-A46BE7",
"outcome": "ap_ready_observed",
"ready_observed": True,
@@ -22220,6 +22217,7 @@ def test_semantic_quick_restart_restores_saved_host_profile_before_endpoint_prob
)
restarted, _ = service_with_fake_runtime(tmp_path)
_select_connection_mode(restarted, "quick-connect")
calls: list[str] = []
association_calls: list[tuple[Path, str, str, float, float]] = []
route_samples: list[HostPathProbeResult] = []
@@ -22367,6 +22365,7 @@ def test_semantic_quick_restart_skips_wifi_mutation_when_direct_route_already_ex
first, _ = service_with_fake_runtime(tmp_path)
_seed_durable_quick_reconnect_evidence(first)
restarted, _ = service_with_fake_runtime(tmp_path)
_select_connection_mode(restarted, "quick-connect")
wifi_mutations: list[str] = []
tcp_calls: list[str] = []
@@ -22444,6 +22443,7 @@ def test_semantic_quick_restart_rejects_non_direct_route_without_tcp_or_ble(
first, _ = service_with_fake_runtime(tmp_path)
_seed_durable_quick_reconnect_evidence(first)
restarted, _ = service_with_fake_runtime(tmp_path)
_select_connection_mode(restarted, "quick-connect")
device_edges: list[str] = []
tcp_calls: list[str] = []
@@ -22528,6 +22528,7 @@ def test_semantic_quick_restart_tcp_timeout_fails_without_ble_fallback(
first, _ = service_with_fake_runtime(tmp_path)
_seed_durable_quick_reconnect_evidence(first)
restarted, _ = service_with_fake_runtime(tmp_path)
_select_connection_mode(restarted, "quick-connect")
device_edges: list[str] = []
tcp_calls: list[str] = []
@@ -22606,6 +22607,7 @@ def test_semantic_quick_restart_rejects_route_change_during_single_tcp_probe(
first, _ = service_with_fake_runtime(tmp_path)
_seed_durable_quick_reconnect_evidence(first)
restarted, _ = service_with_fake_runtime(tmp_path)
_select_connection_mode(restarted, "quick-connect")
device_edges: list[str] = []
tcp_calls: list[str] = []
initial_path = _direct_host_path("192.168.56.1")
@@ -23014,6 +23016,240 @@ def test_semantic_durable_verify_rejects_association_change_during_tcp_probe(
assert state["semantic_topology_store"]["record"]["revision"] == 1
def test_quick_verify_is_rejected_while_bridge_is_selected_before_any_io(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
first, _ = service_with_fake_runtime(tmp_path)
_seed_durable_quick_reconnect_evidence(first)
restarted, _ = service_with_fake_runtime(tmp_path)
edges: list[str] = []
def forbidden_edge(*_: object, **__: object) -> object:
edges.append("io")
raise AssertionError("mode mismatch must fail before BLE, Wi-Fi or endpoint I/O")
async def forbidden_async_edge(*_: object, **__: object) -> dict[str, Any]:
forbidden_edge()
return {}
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_edge)
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_edge)
monkeypatch.setattr(
facade_module,
"associate_with_wifi_profile_once",
forbidden_edge,
)
monkeypatch.setattr(facade_module, "_inspect_host_path", forbidden_edge)
before = restarted.state()
assert before["desired_connection_mode"] == "bridge"
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
asyncio.run(
restarted.verify_connection(
ConnectionVerifyRequest(
device_id=DURABLE_K1_UUID,
source="durable-configured-state",
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
expected_mode_revision=before[
"desired_connection_mode_revision"
],
)
)
)
assert raised.value.reason_code == "connection-mode-draft-mismatch"
assert edges == []
after = restarted.state()
assert after["desired_connection_mode"] == "bridge"
assert after["operations"] == []
def test_physical_recovery_cannot_silently_change_bridge_draft_to_quick(
tmp_path: Path,
) -> None:
original, _ = service_with_fake_runtime(tmp_path)
_persist_resolved_unclassified_stop_for_restart(
original,
connection_mode="quick-connect",
target_ipv4=facade_module.AP_FALLBACK_IPV4,
)
_seed_durable_quick_reconnect_evidence(
original,
transport_ref="test-ble-transport",
)
restarted, _ = service_with_fake_runtime(tmp_path)
before = restarted.state()
assert before["desired_connection_mode"] == "bridge"
assert before["connection_policy"]["actions"][
"observe-configured-device-network"
]["required_connection_mode"] == "quick-connect"
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
restarted.select_connection_mode(
DesiredConnectionModeRequest(
connection_mode="quick-connect",
expected_revision=before["desired_connection_mode_revision"],
)
)
assert raised.value.reason_code == (
"connection-mode-selection-physical-recovery-reset-required"
)
after = restarted.state()
assert after["desired_connection_mode"] == "bridge"
assert after["desired_connection_mode_revision"] == before[
"desired_connection_mode_revision"
]
def test_public_quick_physical_recovery_rejoins_saved_wifi_after_live_ble_proof(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
original, _ = service_with_fake_runtime(tmp_path)
_persist_resolved_unclassified_stop_for_restart(
original,
connection_mode="quick-connect",
target_ipv4=facade_module.AP_FALLBACK_IPV4,
)
_seed_durable_quick_reconnect_evidence(
original,
transport_ref="test-ble-transport",
)
restarted, runtime = service_with_fake_runtime(tmp_path)
# Model a process whose explicit Quick draft was already authoritative
# before the unresolved physical record became visible. The public UI may
# never create this authority as a side effect of the recovery button.
with restarted._lock: # noqa: SLF001
restarted._desired_connection_mode = "quick-connect" # noqa: SLF001
restarted._desired_connection_mode_revision = 1 # noqa: SLF001
restarted._host_wifi_association_probe = FakeHostWifiAssociationProbe( # type: ignore[assignment] # noqa: SLF001
"a" * 64
)
capture = _durable_status_capture(device_id="test-ble-transport")
ordered_edges: list[str] = []
status_reads: list[tuple[str, bool, bool, float | None]] = []
route_samples: list[HostPathProbeResult] = []
network_writes: list[str] = []
async def read_current_quick_status(
device_id: str,
*,
allow_known_device_retrieval: bool = False,
rediscover: bool = False,
exact_scan_timeout_seconds: float | None = None,
on_gatt_validated: Callable[[object], None] | None = None,
**_: object,
) -> dict[str, Any]:
ordered_edges.append("ble-status-read")
status_reads.append(
(
device_id,
allow_known_device_retrieval,
rediscover,
exact_scan_timeout_seconds,
)
)
assert on_gatt_validated is not None
on_gatt_validated(capture)
result = _wifi_status_read(None, device_id=device_id)
result["status"] = _ap_ready_wifi_status()
return result
def associate_saved_profile(*_: object, **__: object) -> dict[str, Any]:
ordered_edges.append("host-wifi-association")
return _successful_saved_quick_profile_association()
def settling_quick_route(target: str) -> HostPathProbeResult:
path = (
_tunnel_host_path(target)
if len(route_samples) < 2
else _direct_host_path(target)
)
route_samples.append(path)
return path
def reachable_tcp(target: str) -> facade_module.TcpReachabilityProbeResult:
assert target == facade_module.AP_FALLBACK_IPV4
ordered_edges.append("mqtt-endpoint-probe")
return facade_module.TcpReachabilityProbeResult(reachable=True)
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
network_writes.append("network-write")
raise AssertionError("saved Quick physical recovery must not write K1 network state")
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_quick_status)
monkeypatch.setattr(
facade_module,
"associate_with_wifi_profile_once",
associate_saved_profile,
)
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
monkeypatch.setattr(facade_module, "pin_connected_device_handle", lambda *_a, **_k: None)
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
monkeypatch.setattr(facade_module, "_inspect_host_path", settling_quick_route)
monkeypatch.setattr(facade_module, "_probe_control_endpoint_socket", reachable_tcp)
monkeypatch.setattr(
facade_module,
"DURABLE_QUICK_CONNECT_ROUTE_SETTLE_INTERVAL_SECONDS",
0.001,
)
_install_real_coordinator_bootstrap(
restarted,
observed_session_state="ready",
)
before = restarted.state()
recovery = before["connection_policy"]["actions"][
"observe-configured-device-network"
]
assert recovery["allowed"] is True
assert recovery["required_connection_mode"] == "quick-connect"
assert recovery["requires_live_gatt_validation"] is True
verified = asyncio.run(restarted.verify_connection(ConnectionVerifyRequest()))
assert ordered_edges == [
"ble-status-read",
"host-wifi-association",
"mqtt-endpoint-probe",
]
assert status_reads == [
(
"test-ble-transport",
True,
True,
facade_module.CONNECTION_VERIFY_EXACT_UUID_SCAN_TIMEOUT_SECONDS,
)
]
assert network_writes == []
assert runtime.start_calls == []
assert runtime.stop_calls == 0
assert verified["active_connection_mode"] == "quick-connect"
assert verified["k1_ip"] == facade_module.AP_FALLBACK_IPV4
assert verified["last_operation"]["status"] == "succeeded"
assert verified["last_operation"]["result"]["physical_reconciliation"][
"performed"
] is True
reassociation_sessions = sorted(
restarted.evidence_root.glob("*viewer_k1_quick_connect_host_reassociation*")
)
assert len(reassociation_sessions) == 1
proof = json.loads(
(reassociation_sessions[0] / "host-network-proof.redacted.json").read_text(
encoding="utf-8"
)
)
assert proof["outcome"] == "direct-endpoint-reachable"
assert proof["route_after"]["route_class"] == "direct"
assert proof["tcp_probe_performed"] is True
assert proof["ble_operation_performed"] is True
assert proof["device_write_performed"] is False
assert proof["automatic_retry"] is False
@pytest.mark.parametrize("observed_session_state", ["ready", "scanning"])
def test_public_physical_recovery_refreshes_stale_dhcp_then_classifies_without_writes(
monkeypatch: pytest.MonkeyPatch,
@@ -27491,9 +27727,9 @@ def test_select_device_handoff_is_local_cancel_invalidates_candidates_and_stale_
expected_reconfiguration_revision=fresh["connection_reconfiguration"]["revision"],
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"]["intent_id"],
)
with pytest.raises(facade_module.NetworkProvisioningConflict) as quick_isolated:
with pytest.raises(facade_module.ConnectionVerificationError) as quick_isolated:
asyncio.run(service.verify_connection(quick_verify_request))
assert quick_isolated.value.reason_code == "connection-reconfiguration-target-mismatch"
assert quick_isolated.value.reason_code == "connection-mode-draft-mismatch"
cancel_request = _reconfiguration_request(fresh, "cancel")
cancelled = asyncio.run(service.prepare_connection_reconfiguration(cancel_request))
@@ -30869,6 +31105,9 @@ def _persist_ambiguous_start_with_prepared_checkpoint(
def _persist_resolved_active_start_for_restart(
service: XgridsK1CompatibilityService,
*,
connection_mode: facade_module.ConnectionMode = "bridge",
target_ipv4: str = "192.168.68.52",
) -> None:
"""Persist one successful START without retaining process-local acquisition."""
@@ -30879,8 +31118,8 @@ def _persist_resolved_active_start_for_restart(
connection = PhysicalCommandConnectionBinding(
intent_id="old-start-intent",
transport_ref="test-ble-transport",
connection_mode="bridge",
target_ipv4="192.168.68.52",
connection_mode=connection_mode,
target_ipv4=target_ipv4,
target_port=facade_module.CONTROL_MQTT_PORT,
host_path_epoch=1,
control_session_id="old-start-control",
@@ -31019,10 +31258,17 @@ def _persist_same_runtime_prepared_checkpoint_for_resolved_start(
def _persist_resolved_unclassified_stop_for_restart(
service: XgridsK1CompatibilityService,
*,
connection_mode: facade_module.ConnectionMode = "bridge",
target_ipv4: str = "192.168.68.52",
) -> tuple[str, int]:
"""Persist the exact legacy startup shape without process-local owners."""
_persist_resolved_active_start_for_restart(service)
_persist_resolved_active_start_for_restart(
service,
connection_mode=connection_mode,
target_ipv4=target_ipv4,
)
ledger = service._physical_command_ledger # noqa: SLF001
start = ledger.snapshot().record
assert start is not None