Стабилизация переключения Bridge и Quick Connect

Безопасно восстанавливает управляющее подключение и локальные checkpoint без повторных команд сканеру.

Добавляет PCAP/Bridge guardrails и регрессионные проверки одношагового переподключения.

Известный дефект: после второго подключения интерфейс не присоединяется к новой генерации preview правой камеры. В живой Quick Connect-сессии STOP был принят, но READY не подтвердился до таймаута; автоматический повтор STOP запрещён.
This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 00:15:28 +03:00
parent 1001a31638
commit 0752e5c6bf
12 changed files with 3481 additions and 234 deletions
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Fail closed if Quick Connect work drifts from K1 protocol or Bridge baselines.
This checker performs local filesystem, Git, and synthetic-test validation only.
It never opens BLE, CoreWLAN, MQTT, RTSP, or a socket to the scanner.
"""
from __future__ import annotations
import hashlib
import os
import stat
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
BASELINE_COMMIT = "1001a316385a742114523c9996c43034898c115d"
@dataclass(frozen=True, slots=True)
class CaptureOracle:
relative_path: str
size_bytes: int
sha256: str
CAPTURE_ORACLES = (
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T151239Z_lixelgo-clean-cycle_d547/"
"captures/iphone-network.pcapng",
126_962_848,
"e6e98e22156ba2ce041c5f6ff32a3116446aaae8b617c90c16b3f00de8d5100f",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T151239Z_lixelgo-clean-cycle_d547/"
"captures/iphone-network.pcap",
116_785_317,
"45ebaebbba8c8f62ebc405eb0f7cdc496d84dcdf80c01decba61c56b4514a26f",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T152407Z_lixelgo-controls-stop_9be0/"
"captures/iphone-network.pcapng",
254_161_132,
"59133299bb1c4dc2157e50401ddef4c131acd2196d7d2e2ccddd5c51b95e57cb",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T152407Z_lixelgo-controls-stop_9be0/"
"captures/iphone-network.pcap",
234_691_027,
"6e267f0ab9b213e5dde732b94cf3f4b2f32fcf66f5569bcb511a13c952883606",
),
)
FROZEN_PATHS = (
"src/k1link/device_plugins/xgrids_k1/protocol",
"src/k1link/device_plugins/xgrids_k1/ble/ap_activation.py",
"src/k1link/device_plugins/xgrids_k1/ble/wifi_provisioning.py",
"src/k1link/device_plugins/xgrids_k1/connection_supervisor.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_coordinator.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_ledger.py",
"src/k1link/device_plugins/xgrids_k1/mqtt",
"src/k1link/device_plugins/xgrids_k1/camera.py",
"src/k1link/device_plugins/xgrids_k1/viewer/runtime.py",
"src/k1link/device_plugins/xgrids_k1/archive.py",
)
PYTEST_TARGETS = (
"tests/test_xgrids_application_bootstrap.py",
(
"tests/test_xgrids_application_mqtt.py::"
"test_unanswered_optional_status_does_not_steal_same_identity_required_refresh"
),
(
"tests/test_xgrids_application_mqtt.py::"
"test_already_arrived_optional_status_is_consumed_before_required_refresh"
),
(
"tests/test_xgrids_application_mqtt.py::"
"test_unbound_optional_status_remains_distinct_from_bound_required_refresh"
),
(
"tests/test_xgrids_application_session.py::"
"test_canonical_stages_require_operator_events_but_device_standby_does_not"
),
(
"tests/test_connection_supervisor.py::"
"test_quick_to_bridge_revokes_authority_with_same_target_and_host_epoch"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_applied_bridge_target_requires_explicit_wifi_client_mode"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_post_dispatch_bridge_accepts_exact_fw302_network_name_when_baseline_is_same"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_post_dispatch_bridge_rejects_another_fw302_network_name"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_bridge_read_only_verify_never_shortcuts_fresh_ble_status"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_durable_restart_same_bridge_baseline_cannot_resolve_ambiguous_write"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_to_bridge_requires_new_explicit_scan_instead_of_retained_session"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_to_bridge_does_not_retrieve_old_handle_without_new_scan"
),
)
def fail(message: str) -> None:
raise RuntimeError(message)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
while chunk := source.read(4 * 1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def verify_capture_oracles() -> None:
for oracle in CAPTURE_ORACLES:
path = REPOSITORY_ROOT / oracle.relative_path
try:
metadata = path.lstat()
except FileNotFoundError:
fail(f"capture oracle is missing: {oracle.relative_path}")
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
fail(f"capture oracle is not a regular non-symlink file: {oracle.relative_path}")
if stat.S_IMODE(metadata.st_mode) != 0o600:
fail(f"capture oracle mode is not 0600: {oracle.relative_path}")
if metadata.st_size != oracle.size_bytes:
fail(
"capture oracle size changed: "
f"{oracle.relative_path} ({metadata.st_size} != {oracle.size_bytes})"
)
observed_sha256 = sha256_file(path)
if observed_sha256 != oracle.sha256:
fail(
"capture oracle SHA-256 changed: "
f"{oracle.relative_path} ({observed_sha256} != {oracle.sha256})"
)
print(f"[k1-guardrail] PASS capture oracles: {len(CAPTURE_ORACLES)}")
def git_output(*args: str) -> str:
result = subprocess.run(
("git", *args),
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def verify_frozen_contour() -> None:
git_output("cat-file", "-e", f"{BASELINE_COMMIT}^{{commit}}")
changed = git_output("diff", "--name-only", BASELINE_COMMIT, "--", *FROZEN_PATHS)
untracked = git_output(
"ls-files",
"--others",
"--exclude-standard",
"--",
*FROZEN_PATHS,
)
drift = tuple(line for line in (changed, untracked) if line)
if drift:
fail("frozen K1 protocol/Bridge contour changed:\n" + "\n".join(drift))
print(
"[k1-guardrail] PASS frozen protocol/Bridge contour: "
f"baseline {BASELINE_COMMIT[:7]}"
)
def run_checked(command: tuple[str, ...]) -> None:
print("[k1-guardrail] RUN " + " ".join(command), flush=True)
subprocess.run(command, cwd=REPOSITORY_ROOT, check=True)
def verify_synthetic_contracts() -> None:
python = REPOSITORY_ROOT / ".venv" / "bin" / "python"
ruff = REPOSITORY_ROOT / ".venv" / "bin" / "ruff"
if not python.is_file():
fail("repository Python runtime is missing: .venv/bin/python")
if not ruff.is_file():
fail("repository Ruff runtime is missing: .venv/bin/ruff")
run_checked((str(python), "-m", "pytest", "-q", *PYTEST_TARGETS))
run_checked(
(
str(ruff),
"check",
"scripts/check_k1_quick_connect_guardrails.py",
"src/k1link/device_plugins/xgrids_k1/facade.py",
"tests/test_xgrids_acquisition_lifecycle.py",
)
)
run_checked(("git", "diff", "--check"))
print("[k1-guardrail] PASS synthetic protocol/Bridge sentinels")
def main() -> int:
os.chdir(REPOSITORY_ROOT)
try:
verify_capture_oracles()
verify_frozen_contour()
verify_synthetic_contracts()
except (OSError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f"[k1-guardrail] FAIL {exc}", file=sys.stderr)
return 1
print("[k1-guardrail] PASS all checks; no scanner I/O was performed")
return 0
if __name__ == "__main__":
raise SystemExit(main())