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/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-17", timeout=3 ) as response: script = response.read().decode("utf-8") with urllib.request.urlopen( f"{self.base_url}/self-repair.css?v=repair-17", timeout=3 ) as response: styles = response.read().decode("utf-8") with urllib.request.urlopen( f"{self.base_url}/self-repair-seat3d.js?v=repair-17", timeout=3 ) as response: seat3d = response.read().decode("utf-8") with urllib.request.urlopen( f"{self.base_url}/console-v2.js?v=desktop-refactor-28", timeout=3 ) as response: console_script = response.read().decode("utf-8") assert 'href="/self-repair.css?v=repair-17"' in html assert 'src="/self-repair.js?v=repair-17"' in html assert 'src="/self-repair-seat3d.js?v=repair-17"' in html assert "量产故障用例" 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 "--repair-deep-blue" in styles assert '.console-v2[data-module="self-repair"] .section-kicker' in styles assert 'grid-template-areas: "twin cases timeline"' in styles assert "background: color-mix(in srgb, var(--v2-accent-soft) 82%, 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("self-repair-seat-reset"' 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