Retain K1 connection between scans and admit the next named acquisition
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user