From d8237f3ffdec242c69b6e3e6631f356f7b7c7a29 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Mon, 19 Oct 2026 22:28:04 +0800 Subject: [PATCH 1/6] fix(runtime): recover a live daemon from a damaged locator A damaged locator was collapsed into no runtime, so the next request cold-started a second daemon while doctor reported stopped. Distinguish a missing locator from an unreadable one, retain the diagnostic and recorded PID liveness in the lifecycle projection, and reuse a recorded endpoint only after an authenticated ping proves the same PID, fingerprint and locator path. Signed-off-by: JasonBuildAI --- loopx/control_plane/effect_runtime.py | 207 ++++++++++++++++-- loopx/doctor.py | 8 + .../test_effect_runtime_locator_recovery.py | 155 +++++++++++++ .../test_turn_journal_runtime_readiness.py | 6 + 4 files changed, 361 insertions(+), 15 deletions(-) create mode 100644 tests/control_plane/test_effect_runtime_locator_recovery.py diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 22e4966316..3e706e8f52 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -17,7 +17,7 @@ from functools import lru_cache from pathlib import Path from threading import Lock -from typing import IO, Any +from typing import IO, Any, Literal, TypeAlias from ..file_lock import process_is_alive from .runtime.file_reads import iter_binary_file_reads @@ -66,6 +66,7 @@ STARTUP_POLL_SECONDS = 0.025 RUNTIME_LOCATOR_PERMISSION_RETRIES = 3 RUNTIME_RETRY_SETTLE_SECONDS = 0.25 +RUNTIME_LOCATOR_RECOVERY_TIMEOUT_SECONDS = 1.0 DEFAULT_REQUEST_TIMEOUT_SECONDS = 10.0 # Canonical writers may wait 30 seconds for the per-Goal maintenance lock and # another 5 seconds for the provider lock. Keep the client connected through @@ -458,7 +459,26 @@ def _start_lock_holder_pid(path: Path) -> int | None: return value if value > 0 else None -def _read_info(path: Path, *, fingerprint: str | None) -> dict[str, Any] | None: +_RuntimeLocatorState: TypeAlias = Literal["missing", "valid", "invalid"] +_RuntimeLocatorDiagnostic: TypeAlias = Literal[ + "runtime_locator_unreadable", + "runtime_locator_schema_mismatch", + "runtime_locator_fingerprint_mismatch", + "runtime_locator_fields_invalid", + "runtime_locator_pid_not_alive", +] + + +@dataclass(frozen=True) +class _RuntimeLocatorRead: + info: dict[str, Any] | None + present: bool + diagnostic_code: _RuntimeLocatorDiagnostic | None = None + recorded_pid_alive: bool | None = None + raw_payload: Mapping[str, Any] | None = None + + +def _inspect_info(path: Path, *, fingerprint: str | None) -> _RuntimeLocatorRead: for attempt in range(RUNTIME_LOCATOR_PERMISSION_RETRIES + 1): try: raw = path.read_text(encoding="utf-8") @@ -469,34 +489,95 @@ def _read_info(path: Path, *, fingerprint: str | None) -> dict[str, Any] | None: # publish the shared filesystem diagnostic through its startup envelope. # A short-lived denial can also overlap locator publication/retirement. if path.is_dir() and not path.is_symlink(): - return None + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_unreadable", + ) if attempt == RUNTIME_LOCATOR_PERMISSION_RETRIES: # Keep symlinks and persistently unreadable metadata on the # host-permission path after the bounded retry window. raise EffectRuntimeHostPermissionError() from exc time.sleep(STARTUP_POLL_SECONDS) + except UnicodeError: + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_unreadable", + ) except (FileNotFoundError, OSError): - return None + return _RuntimeLocatorRead(info=None, present=False) try: payload = json.loads(raw) except json.JSONDecodeError: - return None + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_unreadable", + ) if not isinstance(payload, dict): - return None + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_unreadable", + ) + pid = payload.get("pid") + recorded_pid_alive = ( + _pid_is_alive(pid) + if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0 + else None + ) + if payload.get("schema_version") != EFFECT_RUNTIME_INFO_SCHEMA_VERSION: + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_schema_mismatch", + recorded_pid_alive=recorded_pid_alive, + raw_payload=payload, + ) if ( - payload.get("schema_version") != EFFECT_RUNTIME_INFO_SCHEMA_VERSION - or (fingerprint is not None and payload.get("fingerprint") != fingerprint) + (fingerprint is not None and payload.get("fingerprint") != fingerprint) or (fingerprint is None and ( not isinstance(payload.get("fingerprint"), str) or not BARE_SHA256_PATTERN.fullmatch(payload["fingerprint"]) )) - or payload.get("host") != "127.0.0.1" + ): + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_fingerprint_mismatch", + recorded_pid_alive=recorded_pid_alive, + raw_payload=payload, + ) + if ( + payload.get("host") != "127.0.0.1" or not isinstance(payload.get("port"), int) or not isinstance(payload.get("token"), str) - or not _pid_is_alive(payload.get("pid")) ): - return None - return payload + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_fields_invalid", + recorded_pid_alive=recorded_pid_alive, + raw_payload=payload, + ) + if recorded_pid_alive is not True: + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_pid_not_alive", + recorded_pid_alive=recorded_pid_alive, + raw_payload=payload, + ) + return _RuntimeLocatorRead( + info=payload, + present=True, + recorded_pid_alive=True, + ) + + +def _read_info(path: Path, *, fingerprint: str | None) -> dict[str, Any] | None: + return _inspect_info(path, fingerprint=fingerprint).info def _wait_for_runtime_locator_turnover( @@ -614,7 +695,7 @@ def restart_effect_runtime(*, timeout: float = 5.0, wait_for_exit: bool = False) fingerprint = _runtime_fingerprint() info_path = _runtime_info_path(fingerprint) - info = _read_info(info_path, fingerprint=fingerprint) + info = _runtime_info_for_request(info_path, fingerprint=fingerprint) return _stop_effect_runtime(info_path, info, timeout=timeout, wait_for_process_exit=wait_for_exit) @@ -802,6 +883,79 @@ def _request_with_info( return response +def _recover_runtime_info( + path: Path, + locator: _RuntimeLocatorRead, + *, + fingerprint: str, +) -> dict[str, Any] | None: + """Reuse a live daemon when its locator bytes are damaged but authenticated.""" + + candidate: Mapping[str, Any] | None = locator.info + if candidate is None and locator.present: + # An invalid locator can still be an identity claim when it retains the + # loopback endpoint and token. The authenticated ping below is the + # authority; the damaged bytes are never trusted by themselves. + candidate = locator.raw_payload + if not isinstance(candidate, Mapping): + return None + if ( + candidate.get("host") != "127.0.0.1" + or not isinstance(candidate.get("port"), int) + or not isinstance(candidate.get("token"), str) + or locator.recorded_pid_alive is not True + ): + return None + try: + response = _request_with_info( + candidate, + request_id=str(uuid.uuid4()), + method="runtime.ping", + params={}, + timeout=RUNTIME_LOCATOR_RECOVERY_TIMEOUT_SECONDS, + ) + except (OSError, RuntimeError): + return None + result = response.get("result") + if ( + not isinstance(result, Mapping) + or result.get("ready") is not True + or result.get("pid") != candidate.get("pid") + or result.get("fingerprint") != fingerprint + ): + return None + published = result.get("info_path") + if isinstance(published, str): + try: + same_path = os.path.normcase(str(Path(published).resolve())) == os.path.normcase( + str(path.resolve()) + ) + except OSError: + return None + if not same_path: + return None + return { + **candidate, + "schema_version": EFFECT_RUNTIME_INFO_SCHEMA_VERSION, + "fingerprint": fingerprint, + } + + +def _runtime_info_for_request( + path: Path, + *, + fingerprint: str, +) -> dict[str, Any] | None: + info = _read_info(path, fingerprint=fingerprint) + if info is not None: + return info + return _recover_runtime_info( + path, + _inspect_info(path, fingerprint=fingerprint), + fingerprint=fingerprint, + ) + + def _read_local_snapshot_response( envelope: dict[str, Any], sink: Path | None, *, method: str, request_id: str, timeout: float, ) -> dict[str, Any]: @@ -1074,7 +1228,7 @@ def effect_runtime_request( info: dict[str, Any] | None = None try: try: - info = _read_info(info_path, fingerprint=fingerprint) + info = _runtime_info_for_request(info_path, fingerprint=fingerprint) if info is None: info = _start_runtime(fingerprint=fingerprint, info_path=info_path) except PermissionError as exc: @@ -1161,7 +1315,10 @@ def _serving_runtime_identity() -> dict[str, Any] | None: """Project the identity of the runtime serving this revision, if any.""" fingerprint = _runtime_fingerprint() - info = _read_info(_runtime_info_path(fingerprint), fingerprint=fingerprint) + info = _runtime_info_for_request( + _runtime_info_path(fingerprint), + fingerprint=fingerprint, + ) return runtime_identity_from_info(info) @@ -1191,6 +1348,8 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] runtime_state = "unavailable" runtime_diagnostic_code: str | None = probe.diagnostic_code runtime_identity: dict[str, Any] | None = None + locator_state: _RuntimeLocatorState = "missing" + recorded_pid_alive: bool | None = None host_permission_error: EffectRuntimeHostPermissionError | None = None if ready: try: @@ -1199,6 +1358,22 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] _runtime_info_path(fingerprint), fingerprint=fingerprint, ) + if info is not None: + locator_state = "valid" + recorded_pid_alive = True + else: + locator = _inspect_info( + _runtime_info_path(fingerprint), + fingerprint=fingerprint, + ) + locator_state = "invalid" if locator.present else "missing" + recorded_pid_alive = locator.recorded_pid_alive + runtime_diagnostic_code = locator.diagnostic_code or runtime_diagnostic_code + info = _recover_runtime_info( + _runtime_info_path(fingerprint), + locator, + fingerprint=fingerprint, + ) runtime_state = "running" if info is not None else "stopped" runtime_identity = runtime_identity_from_info(info) except EffectRuntimeHostPermissionError as exc: @@ -1222,6 +1397,8 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] "restart_policy": "automatic_on_next_control_plane_request", "idle_shutdown": True, "diagnostic_code": runtime_diagnostic_code, + "locator_state": locator_state, + "recorded_pid_alive": recorded_pid_alive, } result: dict[str, object] = { "schema_version": EFFECT_RUNTIME_READINESS_SCHEMA_VERSION, diff --git a/loopx/doctor.py b/loopx/doctor.py index 211ce1d85a..fbbec1fd20 100644 --- a/loopx/doctor.py +++ b/loopx/doctor.py @@ -1293,6 +1293,10 @@ def render_doctor_markdown(payload: dict[str, Any]) -> str: f"- typescript_control_plane: `{typescript_control_plane.get('status')}`", f"- typescript_runtime_state: `{typescript_runtime_lifecycle.get('state')}`", f"- typescript_runtime_diagnostic: `{typescript_runtime_lifecycle.get('diagnostic_code')}`", + "- typescript_runtime_locator_state: " + f"`{(typescript_runtime_lifecycle or {}).get('locator_state')}`", + "- typescript_runtime_recorded_pid_alive: " + f"`{(typescript_runtime_lifecycle or {}).get('recorded_pid_alive')}`", "", "## Checks", ] @@ -1399,6 +1403,10 @@ def render_doctor_markdown(payload: dict[str, Any]) -> str: f"- minimum_node_version: `{typescript_control_plane.get('minimum_node_version')}`", f"- detected_node_version: `{typescript_control_plane.get('detected_node_version')}`", f"- semantic_probe: `{typescript_control_plane.get('semantic_probe')}`", + "- locator_state: " + f"`{(typescript_runtime_lifecycle or {}).get('locator_state')}`", + "- recorded_pid_alive: " + f"`{(typescript_runtime_lifecycle or {}).get('recorded_pid_alive')}`", ] ) runtime_identity = typescript_control_plane.get("runtime_identity") diff --git a/tests/control_plane/test_effect_runtime_locator_recovery.py b/tests/control_plane/test_effect_runtime_locator_recovery.py new file mode 100644 index 0000000000..4189cb23f9 --- /dev/null +++ b/tests/control_plane/test_effect_runtime_locator_recovery.py @@ -0,0 +1,155 @@ +"""A damaged locator must not hide or duplicate a live managed runtime.""" +from __future__ import annotations + +import json +import subprocess +import sys +import time +from pathlib import Path +from uuid import uuid4 + +from loopx.control_plane import effect_runtime +from loopx.doctor import render_doctor_markdown + + +def _runtime_info_path() -> Path: + fingerprint = effect_runtime._runtime_fingerprint() + return effect_runtime._runtime_info_path(fingerprint) + + +def _isolate_runtime(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(effect_runtime, "_runtime_dir", lambda: tmp_path / "runtime") + monkeypatch.setenv("LOOPX_EFFECT_RUNTIME_IDLE_MS", "60000") + + +def _shutdown_info(info: dict[str, object]) -> None: + try: + effect_runtime._request_with_info( + info, + request_id=uuid4().hex, + method="runtime.shutdown", + params={}, + timeout=2, + ) + except (OSError, RuntimeError): + return + pid = info.get("pid") + deadline = time.monotonic() + 5 + while effect_runtime._pid_is_alive(pid) and time.monotonic() < deadline: + time.sleep(0.05) + + +def test_corrupt_schema_version_reuses_live_runtime_and_reports_its_liveness( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + first = effect_runtime.effect_runtime_result("runtime.ping", {}) + info_path = _runtime_info_path() + original = json.loads(info_path.read_text(encoding="utf-8")) + try: + info_path.write_text( + json.dumps({**original, "schema_version": "loopx_effect_runtime_info_v1"}), + encoding="utf-8", + ) + + readiness = effect_runtime.collect_effect_runtime_readiness() + second = effect_runtime.effect_runtime_result("runtime.ping", {}) + + lifecycle = readiness["runtime_lifecycle"] + assert readiness["ready"] is True + assert lifecycle["state"] == "running" + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is True + assert lifecycle["diagnostic_code"] == "runtime_locator_schema_mismatch" + assert second["pid"] == first["pid"] + rendered = render_doctor_markdown({"typescript_control_plane": readiness}) + assert "typescript_runtime_locator_state: `invalid`" in rendered + assert "typescript_runtime_recorded_pid_alive: `True`" in rendered + + restarted = effect_runtime.restart_effect_runtime(wait_for_exit=True) + assert restarted["status"] == "stopped" + assert effect_runtime._pid_is_alive(first["pid"]) is False + finally: + _shutdown_info(original) + if info_path.exists(): + try: + replacement = json.loads(info_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + replacement = None + if isinstance(replacement, dict): + _shutdown_info(replacement) + + +def test_unreadable_locator_reports_a_diagnostic_instead_of_looking_absent( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + info_path = _runtime_info_path() + info_path.parent.mkdir(parents=True) + info_path.write_text("not-json", encoding="utf-8") + + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert lifecycle["state"] == "stopped" + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is None + assert lifecycle["diagnostic_code"] == "runtime_locator_unreadable" + + +def test_invalid_utf8_locator_does_not_break_doctor( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + info_path = _runtime_info_path() + info_path.parent.mkdir(parents=True) + info_path.write_bytes(b"\xff\xfe\x00not-a-locator") + + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert lifecycle["state"] == "stopped" + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is None + assert lifecycle["diagnostic_code"] == "runtime_locator_unreadable" + rendered = render_doctor_markdown({"typescript_control_plane": readiness}) + assert "typescript_runtime_locator_state: `invalid`" in rendered + assert "typescript_runtime_diagnostic: `runtime_locator_unreadable`" in rendered + + +def test_dead_recorded_pid_keeps_a_liveness_diagnostic( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + process = subprocess.Popen([sys.executable, "-c", "pass"]) + dead_pid = process.pid + process.wait(timeout=10) + assert effect_runtime._pid_is_alive(dead_pid) is False + + info_path = _runtime_info_path() + info_path.parent.mkdir(parents=True) + info_path.write_text( + json.dumps( + { + "schema_version": effect_runtime.EFFECT_RUNTIME_INFO_SCHEMA_VERSION, + "fingerprint": effect_runtime._runtime_fingerprint(), + "pid": dead_pid, + "host": "127.0.0.1", + "port": 1, + "token": "stale-token", + } + ), + encoding="utf-8", + ) + + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert lifecycle["state"] == "stopped" + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is False + assert lifecycle["diagnostic_code"] == "runtime_locator_pid_not_alive" diff --git a/tests/control_plane/test_turn_journal_runtime_readiness.py b/tests/control_plane/test_turn_journal_runtime_readiness.py index d667f43fb7..2e30e7208f 100644 --- a/tests/control_plane/test_turn_journal_runtime_readiness.py +++ b/tests/control_plane/test_turn_journal_runtime_readiness.py @@ -298,6 +298,8 @@ def test_current_node_standard_probe_does_not_execute_rule( "restart_policy": "automatic_on_next_control_plane_request", "idle_shutdown": True, "diagnostic_code": None, + "locator_state": "missing", + "recorded_pid_alive": None, } @@ -380,6 +382,8 @@ def test_doctor_markdown_projects_runtime_lifecycle_for_app_health() -> None: "runtime_lifecycle": { "state": "unavailable", "diagnostic_code": "runtime_exited_before_ready", + "locator_state": "missing", + "recorded_pid_alive": None, }, }, "checks": [], @@ -387,6 +391,8 @@ def test_doctor_markdown_projects_runtime_lifecycle_for_app_health() -> None: ) assert "typescript_runtime_state: `unavailable`" in rendered + assert "typescript_runtime_locator_state: `missing`" in rendered + assert "typescript_runtime_recorded_pid_alive: `None`" in rendered assert ( "typescript_runtime_diagnostic: `runtime_exited_before_ready`" in rendered ) From bf3bdad6c29ec46395c0371998abac2ade1e5424 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Mon, 19 Oct 2026 22:28:14 +0800 Subject: [PATCH 2/6] docs(runtime): document locator liveness readback Explain locator_state and recorded_pid_alive so operators can distinguish an absent locator from a damaged one that still authenticates a live runtime. Signed-off-by: JasonBuildAI --- docs/guides/installing-loopx.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/guides/installing-loopx.md b/docs/guides/installing-loopx.md index d82581dcba..c8a9ff3226 100644 --- a/docs/guides/installing-loopx.md +++ b/docs/guides/installing-loopx.md @@ -37,6 +37,12 @@ engine. The same doctor projection exposes `runtime_lifecycle.state` as the App can render this projection without inventing a second health model. `stopped` is healthy and means the idle-exited runtime will restart on the next control-plane request. Validate Node before installing or upgrading LoopX. +The lifecycle also reports `locator_state` (`missing`, `valid`, or `invalid`) +and `recorded_pid_alive`. An invalid locator is not treated as an absent one: +if its loopback endpoint and token still authenticate the recorded PID and +source revision, the existing runtime is reused instead of starting a second +daemon. Otherwise the next request cold-starts, while doctor retains the +locator diagnostic for readback. Upgrading from an older Node 22 installation requires restarting the managed runtime with `loopx doctor --restart-runtime` after the new Node is on `PATH`. SQLite remains opt-in and checks the actual embedded SQLite version before @@ -56,6 +62,11 @@ Node 堆栈。先核对本机对应的占用或文件系统问题;诊断不会 修复未知 locator,也不证明所有启动退出都已归因。没有 typed envelope 的退出仍 保留 `runtime_exited_before_ready`。 +中文:`runtime_lifecycle` 同时报告 `locator_state`(`missing`、`valid` 或 +`invalid`)与 `recorded_pid_alive`。无效 locator 不会被当作不存在:若记录的 +loopback 端点与 token 仍能认证相同 PID 和源码修订,则复用现有 runtime,不再 +启动第二个 daemon;否则下一次请求冷启动,同时 doctor 保留 locator 诊断供回读。 + The CI and release lanes use Node.js 24 LTS. Node.js 26 remains a non-blocking forward-compatibility probe and is not a supported-version promise. After the Node.js 22 maintenance window ends, a separate policy change will raise the From 4b340d9e600e5f24630b7b027f087403a908d79f Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Mon, 19 Oct 2026 23:13:52 +0800 Subject: [PATCH 3/6] fix(runtime): refresh locator projection after depth probe Report the post-probe locator state and liveness instead of the pre-probe snapshot. Keep damaged-locator diagnostics on successful deep probes, classify host permission and generic read failures as invalid rather than absent, and cover both outcomes. Signed-off-by: JasonBuildAI --- loopx/control_plane/effect_runtime.py | 102 +++++++++++++----- .../test_effect_runtime_host_permission.py | 2 + .../test_effect_runtime_locator_recovery.py | 62 +++++++++++ 3 files changed, 142 insertions(+), 24 deletions(-) diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 3e706e8f52..124fd038f5 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -505,8 +505,14 @@ def _inspect_info(path: Path, *, fingerprint: str | None) -> _RuntimeLocatorRead present=True, diagnostic_code="runtime_locator_unreadable", ) - except (FileNotFoundError, OSError): + except FileNotFoundError: return _RuntimeLocatorRead(info=None, present=False) + except OSError: + return _RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_unreadable", + ) try: payload = json.loads(raw) except json.JSONDecodeError: @@ -956,6 +962,44 @@ def _runtime_info_for_request( ) +def _inspect_runtime_locator( + fingerprint: str, + *, + recover: bool, +) -> tuple[ + _RuntimeLocatorState, + bool | None, + _RuntimeLocatorDiagnostic | None, + dict[str, Any] | None, +]: + path = _runtime_info_path(fingerprint) + info = _read_info(path, fingerprint=fingerprint) + if info is not None: + return "valid", True, None, info + locator = _inspect_info(path, fingerprint=fingerprint) + return ( + "invalid" if locator.present else "missing", + locator.recorded_pid_alive, + locator.diagnostic_code, + _recover_runtime_info(path, locator, fingerprint=fingerprint) + if recover + else None, + ) + + +def _locator_projection_after_probe( + fingerprint: str, +) -> tuple[_RuntimeLocatorState, bool | None, str | None]: + try: + state, recorded_pid_alive, diagnostic_code, _ = _inspect_runtime_locator( + fingerprint, + recover=False, + ) + except EffectRuntimeHostPermissionError as exc: + return "invalid", None, exc.diagnostic_code + return state, recorded_pid_alive, diagnostic_code + + def _read_local_snapshot_response( envelope: dict[str, Any], sink: Path | None, *, method: str, request_id: str, timeout: float, ) -> dict[str, Any]: @@ -1354,31 +1398,21 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] if ready: try: fingerprint = _runtime_fingerprint() - info = _read_info( - _runtime_info_path(fingerprint), - fingerprint=fingerprint, + ( + locator_state, + recorded_pid_alive, + locator_read_diagnostic, + info, + ) = _inspect_runtime_locator(fingerprint, recover=True) + runtime_diagnostic_code = ( + locator_read_diagnostic or runtime_diagnostic_code ) - if info is not None: - locator_state = "valid" - recorded_pid_alive = True - else: - locator = _inspect_info( - _runtime_info_path(fingerprint), - fingerprint=fingerprint, - ) - locator_state = "invalid" if locator.present else "missing" - recorded_pid_alive = locator.recorded_pid_alive - runtime_diagnostic_code = locator.diagnostic_code or runtime_diagnostic_code - info = _recover_runtime_info( - _runtime_info_path(fingerprint), - locator, - fingerprint=fingerprint, - ) runtime_state = "running" if info is not None else "stopped" runtime_identity = runtime_identity_from_info(info) except EffectRuntimeHostPermissionError as exc: ready = False status = "probe_failed" + locator_state = "invalid" runtime_diagnostic_code = exc.diagnostic_code host_permission_error = exc except (OSError, EffectRuntimeStartupError) as exc: @@ -1427,6 +1461,9 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] ) if not ready or not deep: return result + probe_error: RuntimeError | None = None + ping: object = None + identity: object = None try: ping = effect_runtime_result("runtime.ping", {}) identity = effect_runtime_result( @@ -1439,7 +1476,18 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] }, ) except RuntimeError as exc: - diagnostic_code = getattr(exc, "diagnostic_code", "semantic_probe_failed") + probe_error = exc + ( + locator_state, + recorded_pid_alive, + post_probe_diagnostic, + ) = _locator_projection_after_probe(fingerprint) + if probe_error is not None: + diagnostic_code = getattr( + probe_error, + "diagnostic_code", + "semantic_probe_failed", + ) return { **result, "ready": False, @@ -1449,10 +1497,12 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] **runtime_lifecycle, "state": "unavailable", "diagnostic_code": diagnostic_code, + "locator_state": locator_state, + "recorded_pid_alive": recorded_pid_alive, }, "recommended_action": ( - exc.recommended_action - if isinstance(exc, EffectRuntimeHostPermissionError) + probe_error.recommended_action + if isinstance(probe_error, EffectRuntimeHostPermissionError) else node_probe_remediation(diagnostic_code) or "Run `loopx doctor --deep` again after any concurrent startup " "finishes. If the same diagnostic code remains, reinstall LoopX " @@ -1475,6 +1525,8 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] **runtime_lifecycle, "state": "unavailable", "diagnostic_code": "semantic_probe_shape_mismatch", + "locator_state": locator_state, + "recorded_pid_alive": recorded_pid_alive, }, "recommended_action": ( "Reinstall LoopX and verify the packaged TypeScript runtime with " @@ -1491,7 +1543,9 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] "runtime_lifecycle": { **runtime_lifecycle, "state": "running", - "diagnostic_code": None, + "diagnostic_code": post_probe_diagnostic, + "locator_state": locator_state, + "recorded_pid_alive": recorded_pid_alive, }, "recommended_action": ( _sqlite_restart_recommendation(serving_identity) diff --git a/tests/control_plane/test_effect_runtime_host_permission.py b/tests/control_plane/test_effect_runtime_host_permission.py index b302780143..c57a714a3d 100644 --- a/tests/control_plane/test_effect_runtime_host_permission.py +++ b/tests/control_plane/test_effect_runtime_host_permission.py @@ -253,6 +253,8 @@ def read(path, *args, **kwargs): monkeypatch.setattr(Path, "read_text", read) result = effect_runtime.collect_effect_runtime_readiness(deep=deep) assert result["ready"] is False + assert result["runtime_lifecycle"]["locator_state"] == "invalid" + assert result["runtime_lifecycle"]["recorded_pid_alive"] is None assert result["runtime_lifecycle"]["diagnostic_code"] == "runtime_host_permission_denied" assert "host-approved" in result["recommended_action"] assert "private locator details" not in json.dumps(result) diff --git a/tests/control_plane/test_effect_runtime_locator_recovery.py b/tests/control_plane/test_effect_runtime_locator_recovery.py index 4189cb23f9..c11626f464 100644 --- a/tests/control_plane/test_effect_runtime_locator_recovery.py +++ b/tests/control_plane/test_effect_runtime_locator_recovery.py @@ -1,6 +1,7 @@ """A damaged locator must not hide or duplicate a live managed runtime.""" from __future__ import annotations +import errno import json import subprocess import sys @@ -39,6 +40,16 @@ def _shutdown_info(info: dict[str, object]) -> None: time.sleep(0.05) +def _shutdown_current_runtime() -> None: + fingerprint = effect_runtime._runtime_fingerprint() + info = effect_runtime._read_info( + effect_runtime._runtime_info_path(fingerprint), + fingerprint=fingerprint, + ) + if info is not None: + _shutdown_info(info) + + def test_corrupt_schema_version_reuses_live_runtime_and_reports_its_liveness( tmp_path: Path, monkeypatch, @@ -67,6 +78,14 @@ def test_corrupt_schema_version_reuses_live_runtime_and_reports_its_liveness( assert "typescript_runtime_locator_state: `invalid`" in rendered assert "typescript_runtime_recorded_pid_alive: `True`" in rendered + deep_readiness = effect_runtime.collect_effect_runtime_readiness(deep=True) + deep_lifecycle = deep_readiness["runtime_lifecycle"] + assert deep_readiness["semantic_probe"] == "passed" + assert deep_lifecycle["state"] == "running" + assert deep_lifecycle["locator_state"] == "invalid" + assert deep_lifecycle["recorded_pid_alive"] is True + assert deep_lifecycle["diagnostic_code"] == "runtime_locator_schema_mismatch" + restarted = effect_runtime.restart_effect_runtime(wait_for_exit=True) assert restarted["status"] == "stopped" assert effect_runtime._pid_is_alive(first["pid"]) is False @@ -153,3 +172,46 @@ def test_dead_recorded_pid_keeps_a_liveness_diagnostic( assert lifecycle["locator_state"] == "invalid" assert lifecycle["recorded_pid_alive"] is False assert lifecycle["diagnostic_code"] == "runtime_locator_pid_not_alive" + + +def test_deep_readiness_refreshes_locator_after_starting_runtime( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + try: + readiness = effect_runtime.collect_effect_runtime_readiness(deep=True) + + lifecycle = readiness["runtime_lifecycle"] + assert readiness["ready"] is True + assert readiness["semantic_probe"] == "passed" + assert lifecycle["state"] == "running" + assert lifecycle["locator_state"] == "valid" + assert lifecycle["recorded_pid_alive"] is True + assert lifecycle["diagnostic_code"] is None + finally: + _shutdown_current_runtime() + + +def test_generic_locator_read_error_does_not_look_absent( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + info_path = _runtime_info_path() + original_read_text = Path.read_text + + def read(path, *args, **kwargs): + if path == info_path: + raise OSError(errno.EIO, "private read details") + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", read) + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert lifecycle["state"] == "stopped" + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is None + assert lifecycle["diagnostic_code"] == "runtime_locator_unreadable" + assert "private read details" not in json.dumps(readiness) From 607a787cef6ea7c23c6b793982b96a079126f063 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Mon, 19 Oct 2026 23:51:05 +0800 Subject: [PATCH 4/6] fix(runtime): preserve locator liveness on unavailable hosts Propagate a host-permission denial from authenticated locator recovery instead of treating it as no runtime and cold-starting. Inspect locator state and PID liveness even when the Node probe is unavailable, while keeping readiness unavailable. Add regressions for both boundaries. Signed-off-by: JasonBuildAI --- loopx/control_plane/effect_runtime.py | 41 +++++++++++-------- .../test_effect_runtime_host_permission.py | 29 +++++++++++++ .../test_effect_runtime_locator_recovery.py | 36 ++++++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 124fd038f5..fb910b26c3 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -920,6 +920,8 @@ def _recover_runtime_info( params={}, timeout=RUNTIME_LOCATOR_RECOVERY_TIMEOUT_SECONDS, ) + except EffectRuntimeHostPermissionError: + raise except (OSError, RuntimeError): return None result = response.get("result") @@ -1395,27 +1397,24 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] locator_state: _RuntimeLocatorState = "missing" recorded_pid_alive: bool | None = None host_permission_error: EffectRuntimeHostPermissionError | None = None - if ready: - try: - fingerprint = _runtime_fingerprint() - ( - locator_state, - recorded_pid_alive, - locator_read_diagnostic, - info, - ) = _inspect_runtime_locator(fingerprint, recover=True) - runtime_diagnostic_code = ( - locator_read_diagnostic or runtime_diagnostic_code - ) - runtime_state = "running" if info is not None else "stopped" - runtime_identity = runtime_identity_from_info(info) - except EffectRuntimeHostPermissionError as exc: + locator_info: dict[str, Any] | None = None + try: + fingerprint = _runtime_fingerprint() + ( + locator_state, + recorded_pid_alive, + locator_read_diagnostic, + locator_info, + ) = _inspect_runtime_locator(fingerprint, recover=True) + except EffectRuntimeHostPermissionError as exc: + locator_state = "invalid" + if ready: ready = False status = "probe_failed" - locator_state = "invalid" runtime_diagnostic_code = exc.diagnostic_code host_permission_error = exc - except (OSError, EffectRuntimeStartupError) as exc: + except (OSError, EffectRuntimeStartupError) as exc: + if ready: ready = False status = "package_invalid" runtime_diagnostic_code = getattr( @@ -1423,6 +1422,14 @@ def collect_effect_runtime_readiness(*, deep: bool = False) -> dict[str, object] "diagnostic_code", "packaged_runtime_source_unreadable", ) + else: + if ready: + runtime_diagnostic_code = ( + locator_read_diagnostic or runtime_diagnostic_code + ) + if ready: + runtime_state = "running" if locator_info is not None else "stopped" + runtime_identity = runtime_identity_from_info(locator_info) runtime_lifecycle: dict[str, object] = { "schema_version": "loopx_effect_runtime_lifecycle_v0", "management": "on_demand_managed", diff --git a/tests/control_plane/test_effect_runtime_host_permission.py b/tests/control_plane/test_effect_runtime_host_permission.py index c57a714a3d..8dc642122b 100644 --- a/tests/control_plane/test_effect_runtime_host_permission.py +++ b/tests/control_plane/test_effect_runtime_host_permission.py @@ -62,6 +62,35 @@ def denied(**_kwargs): assert calls == ["startup"] +def test_recovery_permission_denial_does_not_cold_start(tmp_path: Path, monkeypatch) -> None: + locator = tmp_path / "locator.json" + fingerprint = "fixture" + locator.write_text(json.dumps({ + "schema_version": "loopx_effect_runtime_info_v1", + "fingerprint": fingerprint, + "host": "127.0.0.1", + "port": 1, + "token": "private-token", + "pid": os.getpid(), + })) + monkeypatch.setattr( + effect_runtime, "_runtime_fingerprint_for_request", lambda: fingerprint, + ) + monkeypatch.setattr(effect_runtime, "_runtime_info_path", lambda _: locator) + monkeypatch.setattr( + effect_runtime, "_start_runtime", lambda **_: pytest.fail("must not cold start"), + ) + denial = effect_runtime.EffectRuntimeHostPermissionError() + + def denied(*_args, **_kwargs): + raise denial + + monkeypatch.setattr(effect_runtime, "_request_with_info", denied) + with pytest.raises(effect_runtime.EffectRuntimeHostPermissionError) as raised: + effect_runtime.effect_runtime_request("runtime.ping", {}) + assert raised.value is denial + + def test_process_launch_permission_denial_preserves_type_and_releases_lock( tmp_path: Path, monkeypatch, ) -> None: diff --git a/tests/control_plane/test_effect_runtime_locator_recovery.py b/tests/control_plane/test_effect_runtime_locator_recovery.py index c11626f464..44b8c6f95e 100644 --- a/tests/control_plane/test_effect_runtime_locator_recovery.py +++ b/tests/control_plane/test_effect_runtime_locator_recovery.py @@ -3,6 +3,7 @@ import errno import json +import os import subprocess import sys import time @@ -10,6 +11,7 @@ from uuid import uuid4 from loopx.control_plane import effect_runtime +from loopx.control_plane.runtime.node_probe import NodeProbe, NodeProbeOutcome from loopx.doctor import render_doctor_markdown @@ -215,3 +217,37 @@ def read(path, *args, **kwargs): assert lifecycle["recorded_pid_alive"] is None assert lifecycle["diagnostic_code"] == "runtime_locator_unreadable" assert "private read details" not in json.dumps(readiness) + + +def test_readiness_reports_a_live_locator_when_node_is_unavailable( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + monkeypatch.setattr( + effect_runtime, "_probe_node", lambda **_: NodeProbe(NodeProbeOutcome.MISSING), + ) + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + info_path.parent.mkdir(parents=True) + info_path.write_text( + json.dumps({ + "schema_version": effect_runtime.EFFECT_RUNTIME_INFO_SCHEMA_VERSION, + "fingerprint": fingerprint, + "host": "127.0.0.1", + "port": 1, + "token": "fixture-token", + "pid": os.getpid(), + }), + encoding="utf-8", + ) + + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert readiness["ready"] is False + assert readiness["status"] == "missing" + assert lifecycle["state"] == "unavailable" + assert lifecycle["locator_state"] == "valid" + assert lifecycle["recorded_pid_alive"] is True + assert lifecycle["diagnostic_code"] == "node_unavailable" From 9dadb773d781664b0ce6aa489059e3118271e765 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Tue, 20 Oct 2026 07:27:30 +0800 Subject: [PATCH 5/6] fix(runtime): contain oversized locator pid probes Reject out-of-range recorded PIDs before the OS liveness probe and contain platform overflow so damaged locator data cannot crash readiness or the installation-only doctor. Cover both the API projection and the actual console command. Signed-off-by: JasonBuildAI --- loopx/control_plane/effect_runtime.py | 21 +++-- .../test_effect_runtime_locator_recovery.py | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index fb910b26c3..d024f93919 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -67,6 +67,7 @@ RUNTIME_LOCATOR_PERMISSION_RETRIES = 3 RUNTIME_RETRY_SETTLE_SECONDS = 0.25 RUNTIME_LOCATOR_RECOVERY_TIMEOUT_SECONDS = 1.0 +_RUNTIME_PID_MAX = 0xFFFFFFFF DEFAULT_REQUEST_TIMEOUT_SECONDS = 10.0 # Canonical writers may wait 30 seconds for the per-Goal maintenance lock and # another 5 seconds for the provider lock. Keep the client connected through @@ -432,6 +433,20 @@ def _pid_is_alive(value: object) -> bool: return process_is_alive(value) +def _recorded_pid_alive(value: object) -> bool | None: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value <= 0 + or value > _RUNTIME_PID_MAX + ): + return None + try: + return _pid_is_alive(value) + except OverflowError: + return None + + def _reap_exited_runtime_child(info: Mapping[str, Any] | None) -> None: """Let a dead directly spawned child fail the next non-signaling probe. @@ -528,11 +543,7 @@ def _inspect_info(path: Path, *, fingerprint: str | None) -> _RuntimeLocatorRead diagnostic_code="runtime_locator_unreadable", ) pid = payload.get("pid") - recorded_pid_alive = ( - _pid_is_alive(pid) - if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0 - else None - ) + recorded_pid_alive = _recorded_pid_alive(pid) if payload.get("schema_version") != EFFECT_RUNTIME_INFO_SCHEMA_VERSION: return _RuntimeLocatorRead( info=None, diff --git a/tests/control_plane/test_effect_runtime_locator_recovery.py b/tests/control_plane/test_effect_runtime_locator_recovery.py index 44b8c6f95e..f1103b2f4c 100644 --- a/tests/control_plane/test_effect_runtime_locator_recovery.py +++ b/tests/control_plane/test_effect_runtime_locator_recovery.py @@ -2,6 +2,7 @@ from __future__ import annotations import errno +import hashlib import json import os import subprocess @@ -141,6 +142,85 @@ def test_invalid_utf8_locator_does_not_break_doctor( assert "typescript_runtime_diagnostic: `runtime_locator_unreadable`" in rendered +def test_oversized_locator_pid_does_not_escape_readiness( + tmp_path: Path, + monkeypatch, +) -> None: + _isolate_runtime(tmp_path, monkeypatch) + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + info_path.parent.mkdir(parents=True) + info_path.write_text( + json.dumps({ + "schema_version": "loopx_effect_runtime_info_v1", + "fingerprint": fingerprint, + "host": "127.0.0.1", + "port": 1, + "token": "fixture-token", + "pid": 10**30, + }), + encoding="utf-8", + ) + + readiness = effect_runtime.collect_effect_runtime_readiness() + + lifecycle = readiness["runtime_lifecycle"] + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is None + assert lifecycle["diagnostic_code"] == "runtime_locator_schema_mismatch" + rendered = render_doctor_markdown({"typescript_control_plane": readiness}) + assert "typescript_runtime_recorded_pid_alive: `None`" in rendered + + +def test_installation_doctor_keeps_json_for_oversized_locator_pid( + tmp_path: Path, +) -> None: + fingerprint = effect_runtime._runtime_fingerprint() + owner = str(os.getuid()) if hasattr(os, "getuid") else str(Path.home()) + runtime_dir = tmp_path / ( + "loopx-effect-runtime-" + + hashlib.sha256(owner.encode("utf-8")).hexdigest()[:12] + ) + runtime_dir.mkdir() + info_path = runtime_dir / f"runtime-{fingerprint[:16]}.json" + info_path.write_text( + json.dumps({ + "schema_version": "loopx_effect_runtime_info_v1", + "fingerprint": fingerprint, + "host": "127.0.0.1", + "port": 1, + "token": "fixture-token", + "pid": 10**30, + }), + encoding="utf-8", + ) + environment = { + **os.environ, + "TMPDIR": str(tmp_path), + "TEMP": str(tmp_path), + "TMP": str(tmp_path), + } + + loopx_cli = Path(sys.executable).with_name( + "loopx.exe" if os.name == "nt" else "loopx" + ) + completed = subprocess.run( + [str(loopx_cli), "doctor", "--installation-only", "--format", "json"], + cwd=Path(__file__).resolve().parents[2], + env=environment, + capture_output=True, + text=True, + timeout=60, + ) + + assert completed.returncode in (0, 1), completed.stderr + payload = json.loads(completed.stdout) + lifecycle = payload["typescript_control_plane"]["runtime_lifecycle"] + assert lifecycle["locator_state"] == "invalid" + assert lifecycle["recorded_pid_alive"] is None + assert "Traceback" not in completed.stderr + + def test_dead_recorded_pid_keeps_a_liveness_diagnostic( tmp_path: Path, monkeypatch, From 1940477fb7913811db0315547d02ceb82647af8a Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Tue, 20 Oct 2026 08:28:40 +0800 Subject: [PATCH 6/6] test(runtime): pin locator recovery identity rejections Signed-off-by: JasonBuildAI --- .../test_effect_runtime_locator_recovery.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/control_plane/test_effect_runtime_locator_recovery.py b/tests/control_plane/test_effect_runtime_locator_recovery.py index f1103b2f4c..bde4ae6e18 100644 --- a/tests/control_plane/test_effect_runtime_locator_recovery.py +++ b/tests/control_plane/test_effect_runtime_locator_recovery.py @@ -53,6 +53,34 @@ def _shutdown_current_runtime() -> None: _shutdown_info(info) +def _damaged_locator( + *, + fingerprint: str, + pid: int, + token: str = "fixture-token", +) -> effect_runtime._RuntimeLocatorRead: + return effect_runtime._RuntimeLocatorRead( + info=None, + present=True, + diagnostic_code="runtime_locator_schema_mismatch", + recorded_pid_alive=True, + raw_payload={ + "schema_version": "loopx_effect_runtime_info_v1", + "fingerprint": fingerprint, + "pid": pid, + "host": "127.0.0.1", + "port": 12345, + "token": token, + }, + ) + + +def _prepare_recovery(monkeypatch, *, fingerprint: str, pid: int) -> None: + locator = _damaged_locator(fingerprint=fingerprint, pid=pid) + monkeypatch.setattr(effect_runtime, "_read_info", lambda *args, **kwargs: None) + monkeypatch.setattr(effect_runtime, "_inspect_info", lambda *args, **kwargs: locator) + + def test_corrupt_schema_version_reuses_live_runtime_and_reports_its_liveness( tmp_path: Path, monkeypatch, @@ -103,6 +131,90 @@ def test_corrupt_schema_version_reuses_live_runtime_and_reports_its_liveness( _shutdown_info(replacement) +def test_recovery_rejects_a_ping_from_a_different_pid(monkeypatch) -> None: + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + _prepare_recovery(monkeypatch, fingerprint=fingerprint, pid=os.getpid()) + monkeypatch.setattr( + effect_runtime, + "_request_with_info", + lambda *args, **kwargs: { + "result": { + "ready": True, + "pid": os.getpid() + 1, + "fingerprint": fingerprint, + "info_path": str(info_path), + } + }, + ) + + assert effect_runtime._runtime_info_for_request( + info_path, fingerprint=fingerprint + ) is None + + +def test_recovery_rejects_a_token_the_daemon_does_not_accept(monkeypatch) -> None: + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + _prepare_recovery(monkeypatch, fingerprint=fingerprint, pid=os.getpid()) + + def reject(*args, **kwargs): + raise RuntimeError("runtime.ping rejected") + + monkeypatch.setattr(effect_runtime, "_request_with_info", reject) + + assert effect_runtime._runtime_info_for_request( + info_path, fingerprint=fingerprint + ) is None + + +def test_recovery_rejects_a_ping_with_a_different_fingerprint(monkeypatch) -> None: + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + _prepare_recovery(monkeypatch, fingerprint=fingerprint, pid=os.getpid()) + monkeypatch.setattr( + effect_runtime, + "_request_with_info", + lambda *args, **kwargs: { + "result": { + "ready": True, + "pid": os.getpid(), + "fingerprint": "0" * 64, + "info_path": str(info_path), + } + }, + ) + + assert effect_runtime._runtime_info_for_request( + info_path, fingerprint=fingerprint + ) is None + + +def test_recovery_rejects_a_ping_published_at_a_different_locator_path( + tmp_path: Path, + monkeypatch, +) -> None: + fingerprint = effect_runtime._runtime_fingerprint() + info_path = effect_runtime._runtime_info_path(fingerprint) + _prepare_recovery(monkeypatch, fingerprint=fingerprint, pid=os.getpid()) + monkeypatch.setattr( + effect_runtime, + "_request_with_info", + lambda *args, **kwargs: { + "result": { + "ready": True, + "pid": os.getpid(), + "fingerprint": fingerprint, + "info_path": str(tmp_path / "other-runtime.json"), + } + }, + ) + + assert effect_runtime._runtime_info_for_request( + info_path, fingerprint=fingerprint + ) is None + + def test_unreadable_locator_reports_a_diagnostic_instead_of_looking_absent( tmp_path: Path, monkeypatch,