Files
Cognitive_Agent/tests/e2e/test_self_repair_api.py
T

180 lines
6.5 KiB
Python

from __future__ import annotations
import json
import threading
import urllib.error
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
import pytest
from cognitive_agent.application.self_repair import SelfRepairService
from cognitive_agent.auth import AuthConfig, AuthRuntime
from cognitive_agent.server import SeatDemoHandler
def _auth_disabled() -> AuthRuntime:
return AuthRuntime(
AuthConfig(
enabled=False,
username="",
password_hash="",
session_secret=b"",
session_ttl_seconds=3600,
login_attempts=5,
login_window_seconds=300,
requests_per_minute=120,
trust_proxy_headers=False,
)
)
class TestSelfRepairApi:
@classmethod
def setup_class(cls) -> None:
cls.repair = SelfRepairService()
cls.original_repair = SeatDemoHandler.repair
cls.original_auth = SeatDemoHandler.auth
SeatDemoHandler.repair = cls.repair
SeatDemoHandler.auth = _auth_disabled()
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), SeatDemoHandler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
cls.base_url = f"http://127.0.0.1:{cls.server.server_port}"
@classmethod
def teardown_class(cls) -> None:
cls.server.shutdown()
cls.server.server_close()
cls.thread.join(timeout=2)
SeatDemoHandler.repair = cls.original_repair
SeatDemoHandler.auth = cls.original_auth
def request(
self,
path: str,
*,
method: str = "GET",
payload: dict[str, object] | None = None,
role: str | None = "admin",
) -> urllib.request.Request:
headers = {"Accept": "application/json"}
data = None
if role is not None:
headers["X-Cognitive-Agent-Role"] = role
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload).encode("utf-8")
return urllib.request.Request(
f"{self.base_url}{path}", method=method, headers=headers, data=data
)
def test_user_cannot_call_self_repair_api(self) -> None:
for request in (
self.request("/api/self-repair/state", role="user"),
self.request(
"/api/self-repair/healing/start",
method="POST",
payload={"case_id": "FI-FIELD-01", "execution_mode": "shadow"},
role="user",
),
):
with pytest.raises(urllib.error.HTTPError) as caught:
urllib.request.urlopen(request, timeout=3)
assert caught.value.code == 403
body = json.loads(caught.value.read().decode("utf-8"))
assert body["error"] == "admin_required"
def test_admin_can_read_and_start_shadow_repair(self) -> None:
with urllib.request.urlopen(
self.request("/api/self-repair/state"), timeout=3
) as response:
state = json.loads(response.read().decode("utf-8"))
assert state["schema"] == "seat-self-healing-console-state-v1"
assert len(state["scenarios"]) == 6
assert state["hardware"]["connector_enabled_count"] == 21
with urllib.request.urlopen(
self.request(
"/api/self-repair/runtime",
method="POST",
payload={"active": True, "execution_mode": "shadow"},
),
timeout=3,
) as response:
runtime = json.loads(response.read().decode("utf-8"))["runtime"]
assert runtime["active"] is True
with urllib.request.urlopen(
self.request(
"/api/self-repair/healing/start",
method="POST",
payload={"case_id": "FI-FIELD-01", "execution_mode": "shadow"},
),
timeout=3,
) as response:
started = json.loads(response.read().decode("utf-8"))["healing"]
assert started["case_id"] == "FI-FIELD-01"
assert started["execution_mode"] == "shadow"
assert started["injection_scope"] == "SHADOW_TELEMETRY_ONLY"
with urllib.request.urlopen(
self.request(
"/api/self-repair/healing/reset",
method="POST",
payload={"execution_mode": "shadow"},
),
timeout=3,
) as response:
reset = json.loads(response.read().decode("utf-8"))
assert reset["ok"] is True
assert reset["healing"]["phase"] == "IDLE"
def test_rule_self_test_is_available_without_hardware(self) -> None:
with urllib.request.urlopen(
self.request(
"/api/self-repair/hardware/self-test", method="POST", payload={}
),
timeout=5,
) as response:
result = json.loads(response.read().decode("utf-8"))
assert result["ok"] is True
assert result["self_test"]["result"] == "PASS"
def test_frontend_assets_and_role_transition_are_removed(self) -> None:
with urllib.request.urlopen(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with urllib.request.urlopen(
f"{self.base_url}/console-v2.js?v=desktop-refactor-31", timeout=3
) as response:
console_script = response.read().decode("utf-8")
assert "self-repair" not in html
assert '"admin"' not in console_script
for path in (
"/self-repair.js",
"/self-repair.css",
"/self-repair-seat3d.js",
):
with pytest.raises(urllib.error.HTTPError) as missing_asset:
urllib.request.urlopen(f"{self.base_url}{path}", timeout=3)
assert missing_asset.value.code == 404
def test_self_repair_bridge_uses_the_backend_runtime_directory() -> None:
source = (
Path(__file__).resolve().parents[2]
/ "tools"
/ "zcan"
/ "seat_self_repair_bridge.py"
).read_text(encoding="utf-8")
assert '"artifacts", "tmp", "self-repair", "runtime"' in source
assert 'os.path.join(BASE_DIR, "seat_function_runtime")' not in source
assert "A silent or disconnected CAN bus must not kill" in source
assert 'self.message("GUI STOP_ALL发送失败' in source
assert 'self.message("GUI thermal off发送失败' in source
assert "if opcode == 0x01 and self.latest_system is not None" in source
assert "A bounded\n # timed command is deliberately allowed" in source