wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from http.client import BadStatusLine
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from k1link.device_plugins.xgrids_k1 import cli
|
||||
@@ -10,6 +17,36 @@ from k1link.device_plugins.xgrids_k1.cli import app
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class _FakeServeLease:
|
||||
def __init__(self) -> None:
|
||||
self.active = False
|
||||
|
||||
def __enter__(self) -> _FakeServeLease:
|
||||
assert self.active is False
|
||||
self.active = True
|
||||
return self
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
assert self.active is True
|
||||
self.active = False
|
||||
|
||||
|
||||
def _install_fake_serve_lease(
|
||||
monkeypatch: Any,
|
||||
*,
|
||||
error: BaseException | None = None,
|
||||
) -> _FakeServeLease:
|
||||
lease = _FakeServeLease()
|
||||
|
||||
def acquire(_: Path) -> _FakeServeLease:
|
||||
if error is not None:
|
||||
raise error
|
||||
return lease
|
||||
|
||||
monkeypatch.setattr(cli, "_acquire_mission_core_serve_lease", acquire)
|
||||
return lease
|
||||
|
||||
|
||||
def test_help() -> None:
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
@@ -28,11 +65,14 @@ def test_doctor_json() -> None:
|
||||
|
||||
def test_serve_resolves_frontend_from_repository_root(monkeypatch: Any) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
lease = _install_fake_serve_lease(monkeypatch)
|
||||
|
||||
def fake_run(application: str, **kwargs: object) -> None:
|
||||
assert lease.active is True
|
||||
captured.update({"application": application, **kwargs})
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", fake_run)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
@@ -44,6 +84,315 @@ def test_serve_resolves_frontend_from_repository_root(monkeypatch: Any) -> None:
|
||||
"log_level": "info",
|
||||
"access_log": True,
|
||||
}
|
||||
assert lease.active is False
|
||||
|
||||
|
||||
def test_serve_releases_singleton_lease_when_uvicorn_fails(monkeypatch: Any) -> None:
|
||||
lease = _install_fake_serve_lease(monkeypatch)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
|
||||
|
||||
def failed_run(*_: object, **__: object) -> None:
|
||||
assert lease.active is True
|
||||
raise RuntimeError("startup failed")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", failed_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert isinstance(result.exception, RuntimeError)
|
||||
assert lease.active is False
|
||||
|
||||
|
||||
def test_serve_reuses_an_existing_mission_core_backend(monkeypatch: Any) -> None:
|
||||
lease = _install_fake_serve_lease(monkeypatch)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "mission-core")
|
||||
|
||||
def unexpected_run(*_: object, **__: object) -> None:
|
||||
raise AssertionError("a second backend must not be started")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "уже запущен" in result.stdout
|
||||
assert "второй процесс не создан" in result.stdout
|
||||
assert lease.active is False
|
||||
|
||||
|
||||
def test_serve_rejects_an_unrelated_loopback_listener(monkeypatch: Any) -> None:
|
||||
lease = _install_fake_serve_lease(monkeypatch)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "occupied")
|
||||
|
||||
def unexpected_run(*_: object, **__: object) -> None:
|
||||
raise AssertionError("an occupied port must fail before Uvicorn startup")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "уже занят" in result.stdout
|
||||
assert "Mission Core не стал создавать второй backend" in result.stdout
|
||||
assert "--port" not in result.stdout
|
||||
assert lease.active is False
|
||||
|
||||
|
||||
def test_serve_rejects_noncanonical_port_before_lock_or_health(monkeypatch: Any) -> None:
|
||||
def unexpected_call(*_: object, **__: object) -> None:
|
||||
raise AssertionError("noncanonical port must fail before lock or health inspection")
|
||||
|
||||
monkeypatch.setattr(cli, "_acquire_mission_core_serve_lease", unexpected_call)
|
||||
monkeypatch.setattr(cli, "_local_server_status", unexpected_call)
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_call)
|
||||
|
||||
result = runner.invoke(app, ["serve", "--port", "8001"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "только на каноническом порту 8000" in result.stdout
|
||||
assert "Другой локальный backend не создан" in result.stdout
|
||||
|
||||
|
||||
def test_serve_busy_lock_reuses_confirmed_mission_core(monkeypatch: Any) -> None:
|
||||
_install_fake_serve_lease(
|
||||
monkeypatch,
|
||||
error=cli._MissionCoreServeLeaseUnavailable("busy"),
|
||||
)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "mission-core")
|
||||
|
||||
def unexpected_run(*_: object, **__: object) -> None:
|
||||
raise AssertionError("busy singleton lease must not start Uvicorn")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "уже запущен" in result.stdout
|
||||
assert "второй процесс не создан" in result.stdout
|
||||
|
||||
|
||||
def test_serve_busy_lock_reports_in_progress_when_health_is_not_listening(
|
||||
monkeypatch: Any,
|
||||
) -> None:
|
||||
_install_fake_serve_lease(
|
||||
monkeypatch,
|
||||
error=cli._MissionCoreServeLeaseUnavailable("busy"),
|
||||
)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
|
||||
|
||||
def unexpected_run(*_: object, **__: object) -> None:
|
||||
raise AssertionError("busy singleton lease must not start Uvicorn")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "уже запускается или завершает работу" in result.stdout
|
||||
assert "Второй backend не создан" in result.stdout
|
||||
|
||||
|
||||
def test_serve_busy_lock_rejects_unconfirmed_health_listener(monkeypatch: Any) -> None:
|
||||
_install_fake_serve_lease(
|
||||
monkeypatch,
|
||||
error=cli._MissionCoreServeLeaseUnavailable("busy"),
|
||||
)
|
||||
monkeypatch.setattr(cli, "_local_server_status", lambda _: "occupied")
|
||||
|
||||
def unexpected_run(*_: object, **__: object) -> None:
|
||||
raise AssertionError("busy singleton lease must not start Uvicorn")
|
||||
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "канонический health endpoint" in result.stdout
|
||||
assert "подтверждён" in result.stdout
|
||||
assert "Второй backend не создан" in result.stdout
|
||||
|
||||
|
||||
def test_serve_lock_integrity_error_fails_before_health_or_uvicorn(monkeypatch: Any) -> None:
|
||||
_install_fake_serve_lease(
|
||||
monkeypatch,
|
||||
error=cli._MissionCoreServeLeaseError("unsafe lock"),
|
||||
)
|
||||
|
||||
def unexpected_call(*_: object, **__: object) -> None:
|
||||
raise AssertionError("an untrusted lock must block health and Uvicorn")
|
||||
|
||||
monkeypatch.setattr(cli, "_local_server_status", unexpected_call)
|
||||
monkeypatch.setattr(cli.uvicorn, "run", unexpected_call)
|
||||
|
||||
result = runner.invoke(app, ["serve"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "безопасно получить блокировку" in result.stdout
|
||||
assert "Второй backend не создан" in result.stdout
|
||||
|
||||
|
||||
def test_serve_lock_is_stable_private_and_cross_process(tmp_path: Path) -> None:
|
||||
expected_path = tmp_path / ".runtime" / "mission-core" / ".serve.lock"
|
||||
|
||||
with cli._MissionCoreServeLease.acquire(tmp_path) as first:
|
||||
assert first.path == expected_path
|
||||
first_identity = (expected_path.stat().st_dev, expected_path.stat().st_ino)
|
||||
assert expected_path.stat().st_mode & 0o777 == 0o600
|
||||
assert expected_path.parent.stat().st_mode & 0o777 == 0o700
|
||||
child = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import sys; from pathlib import Path; "
|
||||
"from k1link.device_plugins.xgrids_k1.cli import "
|
||||
"_MissionCoreServeLease, _MissionCoreServeLeaseUnavailable; "
|
||||
"root=Path(sys.argv[1]); "
|
||||
"\ntry: lease=_MissionCoreServeLease.acquire(root)"
|
||||
"\nexcept _MissionCoreServeLeaseUnavailable: raise SystemExit(0)"
|
||||
"\nelse: lease.release(); raise SystemExit(1)"
|
||||
),
|
||||
str(tmp_path),
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
assert child.returncode == 0, child.stderr
|
||||
|
||||
assert expected_path.is_file()
|
||||
with cli._MissionCoreServeLease.acquire(tmp_path) as second:
|
||||
assert (second.path.stat().st_dev, second.path.stat().st_ino) == first_identity
|
||||
|
||||
|
||||
def test_serve_lock_rejects_nonprivate_lock_directory(tmp_path: Path) -> None:
|
||||
lock_dir = tmp_path / ".runtime" / "mission-core"
|
||||
lock_dir.mkdir(mode=0o755, parents=True)
|
||||
lock_dir.chmod(0o755)
|
||||
|
||||
with pytest.raises(cli._MissionCoreServeLeaseError, match="not private"):
|
||||
cli._MissionCoreServeLease.acquire(tmp_path)
|
||||
|
||||
|
||||
def test_serve_lock_rejects_symlinked_lock_directory(tmp_path: Path) -> None:
|
||||
runtime_dir = tmp_path / ".runtime"
|
||||
runtime_dir.mkdir()
|
||||
redirected = tmp_path / "redirected"
|
||||
redirected.mkdir(mode=0o700)
|
||||
(runtime_dir / "mission-core").symlink_to(redirected, target_is_directory=True)
|
||||
|
||||
with pytest.raises(cli._MissionCoreServeLeaseError, match="not private"):
|
||||
cli._MissionCoreServeLease.acquire(tmp_path)
|
||||
|
||||
|
||||
class _FakeHealthResponse:
|
||||
def __init__(self, *, status: int = 200, body: bytes = b"") -> None:
|
||||
self.status = status
|
||||
self.body = body
|
||||
|
||||
def read(self, limit: int) -> bytes:
|
||||
assert limit == 16_385
|
||||
return self.body[:limit]
|
||||
|
||||
|
||||
class _FakeHealthConnection:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
response: _FakeHealthResponse | None = None,
|
||||
error: BaseException | None = None,
|
||||
) -> None:
|
||||
self.response = response
|
||||
self.error = error
|
||||
self.request_call: tuple[str, str, dict[str, str]] | None = None
|
||||
self.closed = False
|
||||
|
||||
def request(self, method: str, path: str, *, headers: dict[str, str]) -> None:
|
||||
self.request_call = (method, path, headers)
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
|
||||
def getresponse(self) -> _FakeHealthResponse:
|
||||
assert self.response is not None
|
||||
return self.response
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def _install_fake_health_connection(
|
||||
monkeypatch: Any,
|
||||
connection: _FakeHealthConnection,
|
||||
) -> None:
|
||||
def fake_connection(host: str, port: int, *, timeout: float) -> _FakeHealthConnection:
|
||||
assert (host, port, timeout) == ("127.0.0.1", 8000, 0.75)
|
||||
return connection
|
||||
|
||||
monkeypatch.setattr(cli.http.client, "HTTPConnection", fake_connection)
|
||||
|
||||
|
||||
def test_local_server_status_recognizes_exact_mission_core_health(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(
|
||||
response=_FakeHealthResponse(
|
||||
body=json.dumps({"service": "mission-core-control-plane"}).encode("utf-8")
|
||||
)
|
||||
)
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "mission-core"
|
||||
assert connection.request_call == (
|
||||
"GET",
|
||||
"/api/health",
|
||||
{"Accept": "application/json", "Connection": "close"},
|
||||
)
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_local_server_status_rejects_a_different_health_identity(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(
|
||||
response=_FakeHealthResponse(body=json.dumps({"service": "other"}).encode("utf-8"))
|
||||
)
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "occupied"
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_local_server_status_only_treats_explicit_refusal_as_free(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(error=ConnectionRefusedError())
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "free"
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_local_server_status_fails_closed_on_listener_timeout(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(error=TimeoutError())
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "occupied"
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_local_server_status_fails_closed_on_non_http_listener(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(error=BadStatusLine("not HTTP"))
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "occupied"
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_local_server_status_rejects_oversized_health_payload(monkeypatch: Any) -> None:
|
||||
connection = _FakeHealthConnection(
|
||||
response=_FakeHealthResponse(body=b" " * 16_385)
|
||||
)
|
||||
_install_fake_health_connection(monkeypatch, connection)
|
||||
|
||||
assert cli._local_server_status(8000) == "occupied"
|
||||
assert connection.closed is True
|
||||
|
||||
|
||||
def test_authority_provision_requires_explicit_reviewed_value_confirmation() -> None:
|
||||
@@ -144,3 +493,68 @@ def test_mqtt_capture_cli_uses_bounded_read_only_capture(
|
||||
}
|
||||
assert "messages: 2" in result.stdout
|
||||
assert "subscriptions active" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ledger_status", ["resolved", "corrupt"])
|
||||
def test_cli_wifi_configure_blocks_retired_or_untrusted_audit_before_credentials_and_ble(
|
||||
monkeypatch: Any,
|
||||
tmp_path: Path,
|
||||
ledger_status: str,
|
||||
) -> None:
|
||||
repository_root = tmp_path / "repository"
|
||||
repository_root.mkdir()
|
||||
monkeypatch.setenv("MISSIONCORE_DATA_DIR", str(tmp_path / "private-data"))
|
||||
monkeypatch.setattr(cli, "_configure_ble_process_lease", lambda: repository_root)
|
||||
retired_transport_ref = "F89438FA-55ED-85AD-EED7-734AC84746D8"
|
||||
record = (
|
||||
SimpleNamespace(
|
||||
operator_retirements=(
|
||||
SimpleNamespace(
|
||||
retirement_id="retirement-cli-blocked-target",
|
||||
retired_transport_ref=retired_transport_ref,
|
||||
),
|
||||
),
|
||||
operator_reconciliation_reopens=(),
|
||||
)
|
||||
if ledger_status == "resolved"
|
||||
else None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"PhysicalCommandLedger",
|
||||
lambda _root: SimpleNamespace(
|
||||
snapshot=lambda: SimpleNamespace(status=ledger_status, record=record)
|
||||
),
|
||||
)
|
||||
calls: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"prompt_wifi_credentials",
|
||||
lambda: calls.append("credentials") or ("ssid", "secret"),
|
||||
)
|
||||
|
||||
async def forbidden_provision(*_args: object, **_kwargs: object) -> dict[str, object]:
|
||||
calls.append("ble-write")
|
||||
raise AssertionError("retired CLI target must not reach BLE")
|
||||
|
||||
monkeypatch.setattr(cli, "provision_wifi_once", forbidden_provision)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"ble",
|
||||
"wifi-configure",
|
||||
"--device",
|
||||
retired_transport_ref.lower(),
|
||||
"--out",
|
||||
str(tmp_path / "wifi-result.json"),
|
||||
"--profile",
|
||||
cli.PROFILE_ID,
|
||||
"--confirm-write",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "blocked before credential or device access" in result.stdout
|
||||
assert calls == []
|
||||
assert not (tmp_path / "wifi-result.json").exists()
|
||||
|
||||
Reference in New Issue
Block a user