Retain K1 connection between scans and admit the next named acquisition

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 21:13:17 +03:00
parent ed9a77ba73
commit c6693d6f44
17 changed files with 586 additions and 274 deletions
+1 -1
View File
@@ -158,7 +158,7 @@ def test_private_release_contains_material_only_in_root_private_member(
position += 60 + length + length % 2
with tarfile.open(fileobj=io.BytesIO(members["control.tar.gz"]), mode="r:gz") as archive:
control = archive.extractfile("control").read().decode()
assert "Depends: mission-core-node (>= 0.8.12)" in control
assert "Depends: mission-core-node (>= 0.8.13)" in control
assert "Replaces: mission-core-node (<< 0.8.0)" in control
+17 -1
View File
@@ -438,10 +438,26 @@ def test_applied_wifi_does_not_grant_control_or_start_authority():
assert item["control"]["reason_code"] is None
def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence():
@pytest.mark.parametrize("initial_phase", ["connection-ready", "completed"])
@pytest.mark.parametrize("pending_start", [False, True])
def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence(
initial_phase, pending_start,
):
async def run():
device = bridge()
device.facade.current["application_control_session"]["state"] = initial_phase
sensor = NodeK1Sensor(device, None)
original_invoke = device.invoke
async def invoke(action, parameters, operation_id):
result = await original_invoke(action, parameters, operation_id)
if action == "acquisition.start" and pending_start:
result["application_control_session"]["physical_command"] = {
"requires_reconciliation": True,
}
return result
device.invoke = invoke
item = project_sensor(state(), "node-test")
command = {
"operation_id": "op_" + "a" * 32,
@@ -34792,3 +34792,24 @@ def test_power_loss_during_observation_preserves_audit_and_releases_ownership(
is True
)
assert write_calls == 1
@pytest.mark.parametrize("socket_open", [True, False])
def test_completed_acquisition_does_not_imply_control_loss(tmp_path, socket_open):
service, _ = service_with_fake_runtime(tmp_path)
binding = _seed_supervised_connection(service)
control = {
"state": "completed", "control_socket_open": socket_open,
"verified_control": _verified_control_for_binding(
binding, control_session_id="retained-control-session",
),
}
service._reconcile_connection_supervisor(control, {"source_mode": "idle"}) # noqa: SLF001
snapshot = service._connection_supervisor.snapshot() # noqa: SLF001
assert snapshot.authority.control_allowed is socket_open
assert snapshot.authority.acquisition_start_allowed is socket_open
if socket_open:
service._reconcile_connection_supervisor( # noqa: SLF001
{**control, "state": "failed"}, {"source_mode": "idle"},
)
assert not service._connection_supervisor.snapshot().authority.control_allowed # noqa: SLF001
@@ -367,6 +367,17 @@ def test_canonical_session_owns_start_active_scan_stop_and_save_boundary() -> No
assert response_evidence[0]["operation_key"] == "bootstrap:1:DeviceInfoRequest"
assert response_evidence[-1]["operation_key"] == "modeling:stop"
assert all("payload" not in item for item in response_evidence)
before_batches = list(transport.batches)
standby_pumps = len(transport.maintain_calls)
transport.pre_start_ready = lambda _binding: True
executor.maintain_standby_until_next_acquisition(
lambda: len(transport.maintain_calls) >= standby_pumps + 3,
)
assert transport.batches == before_batches
assert len(transport.maintain_calls) == standby_pumps + 3
# A next dialogue must use a fresh transport, not reset one-shot guards.
with pytest.raises(ApplicationAcceptanceError):
executor.run_connection_stage(orchestrator)
assert APPLICATION_KEY not in str(executor.snapshot())
assert VENDOR_DEVICE_ID not in str(executor.snapshot())
TypeAdapter(JsonValue).validate_python(executor.snapshot())
+74 -1
View File
@@ -372,6 +372,14 @@ class FakeExecutor:
def maintain_post_stop_until_standby(self) -> None:
self.records.append("wait:device-standby")
def maintain_standby_until_next_acquisition(self, requested: Callable[[], bool]) -> None:
while not requested():
if self.transport.state == "closed":
raise RuntimeError("test transport closed")
if not self.transport.proof_fresh:
raise ApplicationControlProofStale("test control proof expired")
threading.Event().wait(0.005)
def snapshot(self) -> dict[str, object]:
return {
"records": list(self.records),
@@ -2160,9 +2168,11 @@ def test_new_explicit_session_waits_for_old_worker_transport_retirement(
_wait_phase(session, "scanning")
session.request_stop(confirmation=_confirmation())
_wait_phase(session, "completed")
assert new_thread.is_alive()
session.close()
new_thread.join(timeout=2.0)
assert not new_thread.is_alive()
assert transports[1].close_calls == 1
assert transports[1].close_calls == 2 # explicit close and idempotent worker cleanup
def test_start_outcome_unknown_blocks_reopen_even_when_transport_is_closed(
@@ -2856,3 +2866,66 @@ def test_pretransport_authority_failure_remains_safe_after_worker_retirement() -
assert failure["diagnostic_snapshot_unavailable"] == []
assert failure["diagnostic_evidence_unavailable"] == []
assert failure["safe_to_retry"] is True
@pytest.fixture(autouse=True)
def close_test_control_sessions(monkeypatch: pytest.MonkeyPatch):
sessions = []
original = InteractiveApplicationControlSession.__init__
def tracked(self, *args, **kwargs):
original(self, *args, **kwargs)
sessions.append(self)
monkeypatch.setattr(InteractiveApplicationControlSession, "__init__", tracked)
yield
for session in sessions:
session.close()
def test_two_named_scans_retain_idle_connection_and_require_new_dialogues(monkeypatch):
FakeExecutor.records = []
monkeypatch.setattr(session_module, "PhysicalAcceptanceDialogueExecutor", FakeExecutor)
transports = []
def create_transport(host):
transport = FakeTransport(host)
transports.append(transport)
return transport
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(), transport_factory=create_transport,
)
session.open(host="192.168.1.20", timezone_name="UTC", connection_binding=_connection_binding())
_wait_phase(session, "connection-ready")
first_checkpoint = None
for project in ("FIRST", "SECOND"):
current = session.snapshot()
if first_checkpoint is not None:
with pytest.raises(ApplicationAcceptanceError):
session.enter_workspace(expected_session_generation=first_checkpoint[0],
expected_state_revision=first_checkpoint[1])
first_checkpoint = (current["session_generation"], current["state_revision"])
session.enter_workspace(expected_session_generation=current["session_generation"],
expected_state_revision=current["state_revision"])
_wait_phase(session, "workspace-ready")
session.open_project_prompt()
_wait_phase(session, "project-ready")
session.request_start(project_name=project, confirmation=_confirmation())
_wait_phase(session, "scanning")
session.request_stop(confirmation=_confirmation())
finished = _wait_phase(session, "completed")
assert finished["control_socket_open"] is True
assert finished["can_enter_workspace"] is True
assert finished["verified_control"]["control_proof_fresh"] is True
before = list(FakeExecutor.records)
threading.Event().wait(0.025)
assert FakeExecutor.records == before # no timer-driven new acquisition
assert transports[-1].state == "ready"
assert len(transports) == (1 if project == "FIRST" else 2)
assert len(transports) == 2
assert transports[0].state == "closed"
assert FakeExecutor.records.count("start:11-14") == 2
assert FakeExecutor.records.count("stop") == 2
transports[-1].proof_fresh = False
_wait_phase(session, "failed")
assert session.snapshot()["control_socket_open"] is False
assert len(transports) == 2 # no reconnect or physical command replay on real loss