feat: update CAN integration and T2G seat firmware
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
@@ -9,9 +10,11 @@ import urllib.request
|
||||
from http.cookiejar import CookieJar
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError
|
||||
from cognitive_agent.server import SeatDemoHandler
|
||||
|
||||
|
||||
@@ -22,8 +25,18 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
# Web regression tests must never inherit a developer's live bench
|
||||
# enablement, especially when local debug mode intentionally skips the
|
||||
# per-request approval token.
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false"},
|
||||
clear=False,
|
||||
):
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service.artifact_root = Path(cls.temporary_directory.name)
|
||||
cls.service = service
|
||||
cls.original_service = SeatDemoHandler.service
|
||||
SeatDemoHandler.service = service
|
||||
cls.original_auth = SeatDemoHandler.auth
|
||||
SeatDemoHandler.auth = AuthRuntime(
|
||||
@@ -53,6 +66,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.server.shutdown()
|
||||
cls.server.server_close()
|
||||
cls.thread.join(timeout=2)
|
||||
SeatDemoHandler.service = cls.original_service
|
||||
SeatDemoHandler.auth = cls.original_auth
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
@@ -99,7 +113,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
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('placeholder="请输入服务端配置令牌"', html)
|
||||
self.assertIn('data-callout="backrest_recline"', html)
|
||||
self.assertIn('data-callout="legrest_extension"', html)
|
||||
self.assertIn('data-callout="backrest_included_angle"', html)
|
||||
@@ -116,7 +130,14 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
self.assertIn("坐垫脚托夹角133度", html)
|
||||
self.assertNotIn('data-axis="height"', html)
|
||||
self.assertEqual(runtime["domain"], "cabin")
|
||||
self.assertFalse(runtime["hardware_execution_allowed"])
|
||||
self.assertEqual(
|
||||
runtime["hardware_execution_allowed"],
|
||||
runtime["hardware"]["configured"],
|
||||
)
|
||||
self.assertEqual(runtime["hardware"]["supported_targets"], ["front_right"])
|
||||
self.assertEqual(
|
||||
runtime["hardware"]["supported_seat_instances"], ["passenger"]
|
||||
)
|
||||
self.assertGreaterEqual(runtime["matrix"]["messages"], 25)
|
||||
self.assertGreaterEqual(runtime["matrix"]["signals"], 128)
|
||||
self.assertEqual(runtime["motion_position_range_pct"], [0, 100])
|
||||
@@ -148,6 +169,13 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
self.assertIn("climate.set_temperature", capability_names)
|
||||
self.assertIn("roof.set_transmittance", capability_names)
|
||||
self.assertIn("ambient_light.set_illumination", capability_names)
|
||||
seat_capabilities = [
|
||||
item for item in runtime["capabilities"] if item["name"].startswith("seat.")
|
||||
]
|
||||
self.assertTrue(seat_capabilities)
|
||||
self.assertTrue(
|
||||
all(item["target_scopes"] == ["passenger"] for item in seat_capabilities)
|
||||
)
|
||||
|
||||
def test_post_request_returns_complete_dry_run(self) -> None:
|
||||
request = urllib.request.Request(
|
||||
@@ -156,7 +184,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "主驾有点热,有点闷",
|
||||
"utterance": "副驾有点热,有点闷",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-e2e-browser",
|
||||
"vehicle_id": "denza_n7",
|
||||
@@ -172,7 +200,171 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
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)
|
||||
self.assertEqual(result["actuator_state"]["passenger"]["ventilation"]["level"], 1)
|
||||
|
||||
def test_hardware_state_endpoint_returns_physical_passenger_snapshot(self) -> None:
|
||||
synchronized = {
|
||||
"status": "succeeded",
|
||||
"conversation_id": "conv-e2e-physical-state",
|
||||
"source": "physical_can",
|
||||
"complete": True,
|
||||
"missing": [],
|
||||
"actuator_state": {
|
||||
"passenger": {
|
||||
"position": {
|
||||
"longitudinal": {"position_pct": 0},
|
||||
"cushion_tilt": {"position_pct": 0},
|
||||
"backrest_recline": {"position_pct": 0},
|
||||
"legrest_height": {"position_pct": 0},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
with patch.object(
|
||||
self.service,
|
||||
"synchronize_hardware_state",
|
||||
return_value=synchronized,
|
||||
) as synchronize:
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/hardware/seat-state?"
|
||||
"conversation_id=conv-e2e-physical-state&vehicle_id=denza_n7",
|
||||
timeout=3,
|
||||
) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
self.assertEqual(payload["source"], "physical_can")
|
||||
self.assertEqual(
|
||||
payload["actuator_state"]["passenger"]["position"]["longitudinal"]["position_pct"],
|
||||
0,
|
||||
)
|
||||
synchronize.assert_called_once_with(
|
||||
"conv-e2e-physical-state",
|
||||
"denza_n7",
|
||||
)
|
||||
|
||||
def test_gpt_http_failure_falls_back_without_returning_502(self) -> None:
|
||||
original_api_key = self.service.openai.api_key
|
||||
self.service.openai.api_key = "test-key"
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "打开副驾座椅通风",
|
||||
"provider": "openai",
|
||||
"conversation_id": "conv-e2e-gpt-fallback",
|
||||
"execution_mode": "dry_run",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=OpenAIIntentError("invalid_api_key"),
|
||||
):
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(response.status, 201)
|
||||
finally:
|
||||
self.service.openai.api_key = original_api_key
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["provider_used"], "local_rules_fallback")
|
||||
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
|
||||
|
||||
def test_frontend_suite_covers_every_physical_seat_capability(self) -> None:
|
||||
suite_path = (
|
||||
REPOSITORY_ROOT
|
||||
/ "tests"
|
||||
/ "fixtures"
|
||||
/ "seat_frontend_capabilities.json"
|
||||
)
|
||||
suite = json.loads(suite_path.read_text(encoding="utf-8"))
|
||||
required = set(suite["required_hardware_capabilities"])
|
||||
covered: set[str] = set()
|
||||
original_api_key = self.service.openai.api_key
|
||||
self.service.openai.api_key = "frontend-suite-test-key"
|
||||
try:
|
||||
for case in suite["cases"]:
|
||||
with self.subTest(case=case["id"]):
|
||||
requests = []
|
||||
for expected in case["operations"]:
|
||||
covered.add(expected["capability"])
|
||||
requests.append(
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": expected["capability"],
|
||||
"target_scope": expected["target"],
|
||||
"operation": "set",
|
||||
"parameters": [
|
||||
{"name": name, "value": value}
|
||||
for name, value in expected["parameters"].items()
|
||||
],
|
||||
"reason": f"frontend test case {case['id']}",
|
||||
}
|
||||
)
|
||||
model_plan = {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": requests,
|
||||
"ambiguities": [],
|
||||
"decision_summary": [f"执行前端测试用例 {case['id']}。"],
|
||||
"confidence": 0.99,
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": case["utterance"],
|
||||
"provider": "openai",
|
||||
"conversation_id": f"conv-frontend-suite-{case['id'].lower()}",
|
||||
"execution_mode": "dry_run",
|
||||
"vehicle_id": "denza_n7",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
return_value=model_plan,
|
||||
):
|
||||
try:
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
self.fail(f"frontend case {case['id']} returned {exc.code}: {detail}")
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operations = result["function_config"]["spec"]["operations"]
|
||||
self.assertEqual(len(operations), len(case["operations"]))
|
||||
self.assertEqual(
|
||||
result["execution_report"]["spec"]["mode"], "dry_run"
|
||||
)
|
||||
self.assertEqual(
|
||||
len(result["execution_report"]["spec"]["results"]),
|
||||
len(case["operations"]),
|
||||
)
|
||||
for expected, actual in zip(case["operations"], operations):
|
||||
self.assertEqual(actual["capability"], expected["capability"])
|
||||
self.assertEqual(
|
||||
actual["target"]["instance"], expected["target"]
|
||||
)
|
||||
for name, value in expected["parameters"].items():
|
||||
self.assertEqual(actual["parameters"][name], value)
|
||||
finally:
|
||||
self.service.openai.api_key = original_api_key
|
||||
|
||||
self.assertEqual(covered, required)
|
||||
safety_ids = {item["id"] for item in suite["safety_cases"]}
|
||||
self.assertEqual(
|
||||
safety_ids,
|
||||
{"SAFE-01", "SAFE-02", "SAFE-03", "RECOVERY-01"},
|
||||
)
|
||||
|
||||
def test_hardware_request_is_rejected_when_server_gate_is_locked(self) -> None:
|
||||
request = urllib.request.Request(
|
||||
@@ -181,7 +373,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "主驾座椅加热开到一档",
|
||||
"utterance": "副驾座椅加热开到一档",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-e2e-hardware-locked",
|
||||
"execution_mode": "hardware",
|
||||
|
||||
Reference in New Issue
Block a user