fix(k1): fence start endpoint handoff
This commit is contained in:
@@ -90,6 +90,7 @@ from k1link.device_plugins.xgrids_k1.network_provisioning_idempotency_journal im
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.physical_command_coordinator import (
|
||||
LedgerPhysicalCommandCoordinator,
|
||||
PhysicalCommandIntentContext,
|
||||
PhysicalCommandRuntimeBinding,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.physical_command_ledger import (
|
||||
@@ -7502,6 +7503,151 @@ def test_plugin_commanded_acquisition_keeps_start_and_stop_as_explicit_actions(
|
||||
)
|
||||
|
||||
|
||||
def test_terminal_prepared_start_without_publish_settles_and_stops_local_capture(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
control = FakeInteractiveControlSession()
|
||||
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
||||
binding = _seed_supervised_connection(
|
||||
service,
|
||||
transport_ref="k1-start-zero-publish",
|
||||
)
|
||||
prepared = service.prepare_acquisition(
|
||||
_prepare_request(
|
||||
project_name="START_NONE",
|
||||
host=binding.target_ipv4,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
acquisition_id = str(prepared["acquisition"]["acquisition_id"])
|
||||
identity = PhysicalCommandIdentity(
|
||||
vendor_device_id_sha256="a" * 64,
|
||||
device_serial_sha256="b" * 64,
|
||||
)
|
||||
assert control.verified_control is not None
|
||||
connection = PhysicalCommandConnectionBinding(
|
||||
intent_id=binding.intent_id,
|
||||
transport_ref=binding.transport_ref,
|
||||
connection_mode=binding.connection_mode,
|
||||
target_ipv4=binding.target_ipv4,
|
||||
target_port=binding.target_port,
|
||||
host_path_epoch=binding.host_path_epoch,
|
||||
control_session_id=str(control.verified_control["control_session_id"]),
|
||||
producer_generation=int(control.verified_control["producer_generation"]),
|
||||
)
|
||||
coordinator = service._physical_command_coordinator # noqa: SLF001
|
||||
coordinator.application_response(
|
||||
ApplicationMqttResponseEvidence(
|
||||
operation_key="bootstrap:start-none:DeviceInfoRequest",
|
||||
response_topic="lixel/application/response/device_info",
|
||||
payload_sha256="c" * 64,
|
||||
modeling_action=None,
|
||||
result_code=None,
|
||||
success=None,
|
||||
observed_at_utc="2026-08-14T13:30:40.000Z",
|
||||
)
|
||||
)
|
||||
coordinator.bind_control_session(
|
||||
PhysicalCommandRuntimeBinding(
|
||||
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
||||
device_serial_sha256=identity.device_serial_sha256,
|
||||
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
||||
intent_id=connection.intent_id,
|
||||
transport_ref=connection.transport_ref,
|
||||
connection_mode=connection.connection_mode,
|
||||
target_ipv4=connection.target_ipv4,
|
||||
target_port=connection.target_port,
|
||||
host_path_epoch=connection.host_path_epoch,
|
||||
control_session_id=connection.control_session_id,
|
||||
producer_generation=connection.producer_generation,
|
||||
)
|
||||
)
|
||||
coordinator.device_status(
|
||||
ApplicationMqttDeviceStatusEvidence(
|
||||
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
||||
device_serial_sha256=identity.device_serial_sha256,
|
||||
session_state="ready",
|
||||
session_state_code=MODELING_STATE_BASE + 300,
|
||||
project_bound=False,
|
||||
project_id_sha256=None,
|
||||
init_ready=False,
|
||||
status_message_sha256="d" * 64,
|
||||
mqtt_retained=False,
|
||||
observed_at_utc="2026-08-14T13:30:41.000Z",
|
||||
)
|
||||
)
|
||||
payload = b"exact-start-zero-publish"
|
||||
envelope = OneShotPublishEnvelope(
|
||||
operation_key="modeling:start",
|
||||
topic="lixel/application/request/modeling",
|
||||
payload=payload,
|
||||
payload_sha256=hashlib.sha256(payload).hexdigest(),
|
||||
payload_bytes=len(payload),
|
||||
qos=2,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
def request_start_after_durable_prepare(**kwargs: object) -> dict[str, object]:
|
||||
context = kwargs["command_context"]
|
||||
observer = kwargs["preparation_checkpoint_observer"]
|
||||
assert isinstance(context, PhysicalCommandIntentContext)
|
||||
assert callable(observer)
|
||||
coordinator.prepare(context, action="start", envelope=envelope)
|
||||
observer("prepared", context, envelope)
|
||||
control._accept_checkpoint( # noqa: SLF001
|
||||
expected_session_generation=kwargs["expected_session_generation"], # type: ignore[arg-type]
|
||||
expected_state_revision=kwargs["expected_state_revision"], # type: ignore[arg-type]
|
||||
)
|
||||
control.start_projects.append(str(kwargs["project_name"]))
|
||||
control.start_contexts.append(context)
|
||||
control.state = "start-requested"
|
||||
return control.snapshot()
|
||||
|
||||
control.request_start = request_start_after_durable_prepare # type: ignore[method-assign]
|
||||
starting = service.start_acquisition(
|
||||
_start_request(
|
||||
acquisition_id=acquisition_id,
|
||||
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
||||
)
|
||||
)
|
||||
start_operation_id = str(starting["last_operation"]["operation_id"])
|
||||
assert starting["physical_command"]["record"]["stage"] == "prepared"
|
||||
assert starting["active_acquisition_recovery_checkpoint"]["state"] == "prepared"
|
||||
assert runtime.source_mode == "live"
|
||||
|
||||
control.state = "failed"
|
||||
control.state_revision += 1
|
||||
control.failure = {
|
||||
"reason_code": "application-connection-binding-lost",
|
||||
"failed_phase": "start-requested",
|
||||
"modeling_command_attempted": False,
|
||||
"diagnostic_evidence_unavailable": [],
|
||||
"safe_to_retry": True,
|
||||
}
|
||||
control.outcome_unknown = False
|
||||
settled = service.state()
|
||||
|
||||
record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
||||
checkpoint = service._active_acquisition_checkpoint.snapshot().checkpoint # type: ignore[union-attr] # noqa: SLF001
|
||||
operation = service._operations.get(start_operation_id) # noqa: SLF001
|
||||
assert record is not None
|
||||
assert record.stage == "resolved"
|
||||
assert record.resolution == "not-dispatched"
|
||||
assert checkpoint is not None and checkpoint.state == "ceased"
|
||||
assert settled["acquisition"]["state"] == "failed"
|
||||
assert settled["acquisition"]["result"]["device_start"] == "not-dispatched"
|
||||
assert settled["acquisition"]["result"]["receiver_stopped"] is True
|
||||
assert operation.status == "failed"
|
||||
assert operation.stage_code == "physical-start-not-dispatched"
|
||||
assert operation.error is not None
|
||||
assert operation.error["side_effect_status"] == "none"
|
||||
assert operation.error["physical_command_sent"] is False
|
||||
assert runtime.stop_calls == 1
|
||||
assert runtime.source_mode == "idle"
|
||||
assert service._acquisition_session_lease is None # noqa: SLF001
|
||||
|
||||
|
||||
def _activate_real_checkpoint_for_prepared_stop_fixture(
|
||||
service: XgridsK1CompatibilityService,
|
||||
*,
|
||||
@@ -12879,6 +13025,11 @@ def test_public_commanded_workflow_refreshes_stable_route_before_ttl_reduction(
|
||||
control, binding, ready = _install_binding_validating_ready_control(service)
|
||||
stable_path = _direct_host_path(binding.target_ipv4)
|
||||
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"_probe_control_endpoint_socket",
|
||||
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
||||
)
|
||||
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
||||
|
||||
def advance_past_fifteen_seconds_with_fresh_endpoint() -> None:
|
||||
@@ -13244,6 +13395,82 @@ def test_start_refreshes_ttl_expired_endpoint_after_project_prepare(
|
||||
assert control.start_projects == [PROJECT_NAME]
|
||||
|
||||
|
||||
def test_start_refreshes_endpoint_before_prepared_worker_handoff_window(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
monotonic_now = [100.0]
|
||||
suspend_aware_now = [1_000.0]
|
||||
supervisor = service._connection_supervisor # noqa: SLF001
|
||||
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
||||
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
||||
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
||||
control, binding, ready = _install_binding_validating_ready_control(service)
|
||||
stable_path = _direct_host_path(binding.target_ipv4)
|
||||
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
||||
workspace = service.enter_application_workspace(
|
||||
EnterApplicationWorkspaceRequest(
|
||||
operator_confirmed=True,
|
||||
expected_session_generation=ready["application_control_session"][
|
||||
"session_generation"
|
||||
],
|
||||
expected_state_revision=ready["application_control_session"]["state_revision"],
|
||||
)
|
||||
)
|
||||
prepared = service.prepare_acquisition(
|
||||
_prepare_request(
|
||||
project_name=PROJECT_NAME,
|
||||
host=binding.target_ipv4,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
expected_control_session_generation=workspace["application_control_session"][
|
||||
"session_generation"
|
||||
],
|
||||
expected_control_state_revision=workspace["application_control_session"][
|
||||
"state_revision"
|
||||
],
|
||||
)
|
||||
)
|
||||
# The endpoint is still nominally reachable, but only eleven seconds of
|
||||
# its lease remain. That is insufficient for PREPARED + worker dispatch.
|
||||
monotonic_now[0] += 4.01
|
||||
suspend_aware_now[0] += 4.01
|
||||
assert supervisor.snapshot().endpoint.tcp_state == "reachable"
|
||||
tcp_samples: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"_probe_control_endpoint_socket",
|
||||
lambda target: (
|
||||
tcp_samples.append(target)
|
||||
or facade_module.TcpReachabilityProbeResult(reachable=True)
|
||||
),
|
||||
)
|
||||
|
||||
started = service.start_acquisition(
|
||||
_start_request(
|
||||
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
||||
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
||||
expected_control_session_generation=prepared["application_control_session"][
|
||||
"session_generation"
|
||||
],
|
||||
expected_control_state_revision=prepared["application_control_session"][
|
||||
"state_revision"
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert started["acquisition"]["state"] == "starting"
|
||||
assert tcp_samples == [binding.target_ipv4]
|
||||
assert supervisor.endpoint_observation_has_remaining_lease(
|
||||
target=EndpointTarget(binding.target_ipv4, binding.target_port),
|
||||
intent_id=binding.intent_id,
|
||||
host_path_epoch=binding.host_path_epoch,
|
||||
minimum_remaining_seconds=facade_module.COMMAND_ENDPOINT_MIN_REMAINING_SECONDS,
|
||||
)
|
||||
assert len(runtime.start_calls) == 1
|
||||
assert control.start_projects == [PROJECT_NAME]
|
||||
|
||||
|
||||
def test_start_stale_control_proof_fails_before_receiver_or_device_checkpoint(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
||||
Reference in New Issue
Block a user