init
This commit is contained in:
Executable
+181
@@ -0,0 +1,181 @@
|
||||
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('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])
|
||||
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",
|
||||
}
|
||||
).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["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()
|
||||
Reference in New Issue
Block a user