Files
Cognitive_Agent/tests/e2e/test_self_repair_api.py
T
2026-08-14 15:01:29 +08:00

230 lines
9.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_served(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}/self-repair.js?v=repair-21", timeout=3
) as response:
script = response.read().decode("utf-8").replace("\r\n", "\n")
with urllib.request.urlopen(
f"{self.base_url}/self-repair.css?v=repair-21", timeout=3
) as response:
styles = response.read().decode("utf-8")
with urllib.request.urlopen(
f"{self.base_url}/self-repair-seat3d.js?v=repair-21", timeout=3
) as response:
seat3d = 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 'href="/self-repair.css?v=repair-21"' in html
assert 'src="/self-repair.js?v=repair-21"' in html
assert 'class="v2-module-heading"' in script
assert 'SELF-REPAIR' in script
assert 'padding: var(--v2-module-title-block) 0 0' in styles
assert 'margin-inline: var(--v2-module-title-inline)' in styles
assert 'src="/self-repair-seat3d.js?v=repair-21"' in html
assert "量产故障注入" in script
assert "运行与自修复日志" in script
assert "启动座椅运行" in script
assert "开始正常任务" not in script
assert 'id="repairSeatCanvas"' in script
assert 'id="repairSeatViewer"' in script
assert "self-repair-seat-reset" in script
assert "seat-healing-state" in script
assert 'id="repairConfirmDialog"' in script
assert "confirmAction(" in script
assert "window.confirm" not in script
assert 'hardware.link?.adapter === "OK"' in script
assert 'PASSED: ["21针通过", "online"]' in script
assert "connectorRows" in script
assert "Polling refreshes data only" in script
assert "renderRecords();\n showView(activeView);" not in script
assert 'let latest = fallbackState();' in script
assert 'renderAll();\n window.lucide?.createIcons' in script
assert '"WDT复位后失能"' in script
assert "--repair-deep-blue" in styles
assert '.console-v2[data-module="self-repair"] .section-kicker' in styles
assert 'grid-template-areas: "twin cases logs"' in styles
assert ".v2-repair-event-stream" in styles
assert "background: rgba(255, 255, 255, 0.94)" in styles
assert "border-color: color-mix(in srgb, var(--v2-accent) 18%, white)" in styles
assert ".v2-repair-toolbar button.primary:hover:not(:disabled)" in styles
assert "color: #fff;" in styles
assert "seat-healing-state" in seat3d
assert "function backrestShape" in seat3d
assert "new THREE.CapsuleGeometry" in seat3d
assert 'cssThemeColor("--v2-accent", "#315eaa")' in seat3d
assert 'viewer.addEventListener("pointerdown"' in seat3d
assert 'window.addEventListener("pointermove"' in seat3d
assert "window.addEventListener(resetEvent" in seat3d
assert "function viewerHasLayout(viewer)" in seat3d
assert "function deferSeat3dUntilVisible(viewer, options)" in seat3d
assert "if (!viewerHasLayout(viewer))" in seat3d
assert 'window.addEventListener("cognitive-agent:module-change"' in seat3d
assert 'canvas.addEventListener("webglcontextlost"' in seat3d
assert '["self-calibration", "self-repair"].includes(module)' in console_script
assert 'roleSelect.disabled = ui.activeModule === "self-repair"' in console_script
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