from __future__ import annotations import json import tempfile import threading import unittest import urllib.error import urllib.request from http.cookiejar import CookieJar from http.server import ThreadingHTTPServer from pathlib import Path from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService from cognitive_agent.server import SeatDemoHandler class SeatWebApiTests(unittest.TestCase): username = "test-user" password = "public-demo-test-password" @classmethod def setUpClass(cls) -> None: cls.temporary_directory = tempfile.TemporaryDirectory() service = SeatDemoService(REPOSITORY_ROOT) service.artifact_root = Path(cls.temporary_directory.name) SeatDemoHandler.service = service cls.original_auth = SeatDemoHandler.auth SeatDemoHandler.auth = AuthRuntime( AuthConfig( enabled=True, username=cls.username, password_hash=hash_password( cls.password, salt=b"0123456789abcdef", iterations=100_000, ), session_secret=b"t" * 64, session_ttl_seconds=3600, login_attempts=5, login_window_seconds=300, requests_per_minute=20, trust_proxy_headers=False, ) ) 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 tearDownClass(cls) -> None: cls.server.shutdown() cls.server.server_close() cls.thread.join(timeout=2) SeatDemoHandler.auth = cls.original_auth cls.temporary_directory.cleanup() def setUp(self) -> None: self.cookie_jar = CookieJar() self.opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(self.cookie_jar) ) request = urllib.request.Request( f"{self.base_url}/api/auth/login", method="POST", headers={"Content-Type": "application/json"}, data=json.dumps( {"username": self.username, "password": self.password} ).encode("utf-8"), ) with self.opener.open(request, timeout=3) as response: payload = json.loads(response.read().decode("utf-8")) self.assertTrue(payload["authenticated"]) def test_authentication_protects_console_and_api(self) -> None: unauthenticated = urllib.request.build_opener() with unauthenticated.open(f"{self.base_url}/", timeout=3) as response: login_html = response.read().decode("utf-8") final_url = response.geturl() self.assertIn("/login", final_url) self.assertIn("体验账号登录", login_html) with self.assertRaises(urllib.error.HTTPError) as caught: unauthenticated.open(f"{self.base_url}/api/runtime", timeout=3) self.assertEqual(caught.exception.code, 401) def test_static_console_and_runtime_endpoint(self) -> None: with self.opener.open(f"{self.base_url}/", timeout=3) as response: html = response.read().decode("utf-8") with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response: runtime = json.loads(response.read().decode("utf-8")) self.assertIn("Cognitive Agent", html) self.assertIn('id="seatTwinCanvas"', html) self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html) self.assertIn("整车孪生界面", html) self.assertIn("透明车体 · 实体设备", html) self.assertIn("FBXLoader.js", html) self.assertIn('data-seat-layout="five-seat"', html) self.assertIn('data-active-seat="front-right"', html) self.assertIn('placeholder="请输入 123456"', html) self.assertIn('data-callout="backrest_recline"', html) self.assertIn('data-callout="legrest_extension"', html) self.assertIn('data-callout="backrest_included_angle"', html) self.assertIn('data-callout="legrest_included_angle"', html) self.assertIn('data-twin-function="ventilation"', html) self.assertIn('data-twin-function="heating"', html) self.assertIn('class="twin-level-meter"', html) self.assertIn('id="presetIndicator"', html) self.assertIn('id="vehicleSelect"', html) self.assertIn('id="saveModeToggle"', html) self.assertIn('data-domain-panel="climate"', html) self.assertIn('data-domain-panel="roof"', html) self.assertIn('data-domain-panel="ambient_light"', html) self.assertIn("坐垫脚托夹角133度", html) self.assertNotIn('data-axis="height"', html) self.assertEqual(runtime["domain"], "cabin") self.assertFalse(runtime["hardware_execution_allowed"]) self.assertGreaterEqual(runtime["matrix"]["messages"], 25) self.assertGreaterEqual(runtime["matrix"]["signals"], 128) self.assertEqual(runtime["motion_position_range_pct"], [0, 100]) self.assertEqual(runtime["default_vehicle_id"], "denza_n7") self.assertEqual(runtime["vehicle"]["name"], "腾势 N7") self.assertIn( "denza_n7", {vehicle["id"] for vehicle in runtime["vehicles"]}, ) self.assertIn( "zero_gravity", {mode["id"] for mode in runtime["modes"]}, ) self.assertEqual( runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"], 215, ) self.assertEqual( runtime["vehicle"]["seat"]["motors"]["longitudinal"]["reference_zero"], "forwardmost", ) self.assertEqual( runtime["vehicle"]["seat"]["motors"]["cushion_tilt"]["reference_zero"], "highest", ) capability_names = {item["name"] for item in runtime["capabilities"]} self.assertIn("seat.save_posture_preset", capability_names) self.assertIn("seat.apply_posture_preset", capability_names) self.assertIn("climate.set_temperature", capability_names) self.assertIn("roof.set_transmittance", capability_names) self.assertIn("ambient_light.set_illumination", capability_names) def test_post_request_returns_complete_dry_run(self) -> None: request = urllib.request.Request( f"{self.base_url}/api/requests", method="POST", headers={"Content-Type": "application/json"}, data=json.dumps( { "utterance": "主驾有点热,有点闷", "provider": "local", "conversation_id": "conv-e2e-browser", "vehicle_id": "denza_n7", } ).encode("utf-8"), ) with self.opener.open(request, timeout=3) as response: result = json.loads(response.read().decode("utf-8")) self.assertEqual(result["status"], "succeeded") self.assertEqual(result["function_config"]["kind"], "FunctionConfig") self.assertEqual(result["command_plan"]["kind"], "CommandPlan") self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run") self.assertEqual(result["conversation_id"], "conv-e2e-browser") self.assertEqual(result["vehicle"]["id"], "denza_n7") self.assertEqual(result["actuator_state"]["driver"]["ventilation"]["level"], 1) def test_hardware_request_is_rejected_when_server_gate_is_locked(self) -> None: request = urllib.request.Request( f"{self.base_url}/api/requests", method="POST", headers={"Content-Type": "application/json"}, data=json.dumps( { "utterance": "主驾座椅加热开到一档", "provider": "local", "conversation_id": "conv-e2e-hardware-locked", "execution_mode": "hardware", "approval_token": "must-not-be-persisted", } ).encode("utf-8"), ) with self.assertRaises(urllib.error.HTTPError) as caught: self.opener.open(request, timeout=3) self.assertEqual(caught.exception.code, 403) payload = json.loads(caught.exception.read().decode("utf-8")) self.assertEqual(payload["error"], "hardware_safety_gate") artifact_text = "\n".join( path.read_text(encoding="utf-8") for path in Path(self.temporary_directory.name).rglob("*") if path.is_file() ) self.assertNotIn("must-not-be-persisted", artifact_text) if __name__ == "__main__": unittest.main()