feat: integrate N7Cockpit remote mode control
This commit is contained in:
@@ -47,6 +47,8 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.service = service
|
||||
cls.original_service = SeatDemoHandler.service
|
||||
SeatDemoHandler.service = service
|
||||
cls.original_device_token = os.environ.get("COGNITIVE_AGENT_DEVICE_API_TOKEN")
|
||||
os.environ["COGNITIVE_AGENT_DEVICE_API_TOKEN"] = "test-device-api-token"
|
||||
cls.original_auth = SeatDemoHandler.auth
|
||||
SeatDemoHandler.auth = AuthRuntime(
|
||||
AuthConfig(
|
||||
@@ -77,6 +79,10 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.thread.join(timeout=2)
|
||||
SeatDemoHandler.service = cls.original_service
|
||||
SeatDemoHandler.auth = cls.original_auth
|
||||
if cls.original_device_token is None:
|
||||
os.environ.pop("COGNITIVE_AGENT_DEVICE_API_TOKEN", None)
|
||||
else:
|
||||
os.environ["COGNITIVE_AGENT_DEVICE_API_TOKEN"] = cls.original_device_token
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
def setUp(self) -> None:
|
||||
@@ -177,18 +183,18 @@ operations:
|
||||
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}/app.js?v=dual-seat-67", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-69", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3
|
||||
) as response:
|
||||
gesture_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.js?v=desktop-refactor-28", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=desktop-refactor-30", timeout=3
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.css?v=desktop-refactor-87", timeout=3
|
||||
f"{self.base_url}/console-v2.css?v=desktop-refactor-89", timeout=3
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -219,15 +225,16 @@ operations:
|
||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||
self.assertNotIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-67"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-69"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-9"', html)
|
||||
self.assertIn('src="/gesture-control.js?v=android-pad-7"', html)
|
||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||
self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
|
||||
self.assertIn('href="/console-v2.css?v=desktop-refactor-87"', html)
|
||||
self.assertIn('src="/console-v2.js?v=desktop-refactor-30"', html)
|
||||
self.assertIn('href="/console-v2.css?v=desktop-refactor-89"', html)
|
||||
self.assertIn('src="/self-extension.js?v=extension-18"', html)
|
||||
self.assertIn('href="/self-extension.css?v=extension-12"', html)
|
||||
self.assertNotIn("滚动查看更多", html)
|
||||
|
||||
self.assertIn('aria-label="执行车舱控制指令"', html)
|
||||
self.assertIn('sessionStorage.getItem("cognitive-agent.demo-role")', console_script)
|
||||
self.assertIn('id="v2SidebarToggle"', console_script)
|
||||
@@ -534,13 +541,153 @@ operations:
|
||||
)
|
||||
)
|
||||
|
||||
def test_remote_mode_api_executes_repository_preset_and_publishes_events(self) -> None:
|
||||
modes_url = f"{self.base_url}/api/v1/vehicles/denza_n7/modes"
|
||||
with self.opener.open(modes_url, timeout=3) as response:
|
||||
catalog = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(catalog["vehicle_id"], "denza_n7")
|
||||
self.assertIn("zero_gravity", {mode["id"] for mode in catalog["modes"]})
|
||||
|
||||
create = urllib.request.Request(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": "web-api-test-zero-gravity",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"mode_id": "zero_gravity",
|
||||
"execution_target": "dry_run",
|
||||
"source": "android",
|
||||
"source_device_id": "n7-cockpit-test",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(create, timeout=3) as response:
|
||||
accepted = json.loads(response.read().decode("utf-8"))
|
||||
execution_id = accepted["execution_id"]
|
||||
self.assertEqual(accepted["mode_id"], "zero_gravity")
|
||||
|
||||
terminal = None
|
||||
for _ in range(100):
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/v1/mode-executions/{execution_id}", timeout=3
|
||||
) as response:
|
||||
terminal = json.loads(response.read().decode("utf-8"))
|
||||
if terminal["status"] in {
|
||||
"succeeded", "partially_succeeded", "failed", "cancelled", "timed_out"
|
||||
}:
|
||||
break
|
||||
threading.Event().wait(0.02)
|
||||
self.assertIsNotNone(terminal)
|
||||
self.assertEqual(terminal["status"], "succeeded")
|
||||
self.assertEqual(terminal["result"]["matched_modes"][0]["id"], "zero_gravity")
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/events?after=0", timeout=3
|
||||
) as response:
|
||||
events = json.loads(response.read().decode("utf-8"))
|
||||
matching = [
|
||||
event for event in events["events"]
|
||||
if event["execution"]["execution_id"] == execution_id
|
||||
]
|
||||
self.assertGreaterEqual(len(matching), 2)
|
||||
self.assertEqual(matching[-1]["execution"]["status"], "succeeded")
|
||||
|
||||
nap_create = urllib.request.Request(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"mode_id": "nap",
|
||||
"execution_target": "dry_run",
|
||||
"source": "android",
|
||||
"source_device_id": "n7-cockpit-test",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(nap_create, timeout=3) as response:
|
||||
nap_accepted = json.loads(response.read().decode("utf-8"))
|
||||
nap_terminal = None
|
||||
for _ in range(100):
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/v1/mode-executions/"
|
||||
f"{nap_accepted['execution_id']}",
|
||||
timeout=3,
|
||||
) as response:
|
||||
nap_terminal = json.loads(response.read().decode("utf-8"))
|
||||
if nap_terminal["status"] in {
|
||||
"succeeded", "partially_succeeded", "failed", "cancelled", "timed_out"
|
||||
}:
|
||||
break
|
||||
threading.Event().wait(0.02)
|
||||
self.assertEqual(nap_terminal["status"], "succeeded")
|
||||
self.assertEqual(
|
||||
[mode["id"] for mode in nap_terminal["result"]["matched_modes"]],
|
||||
["nap"],
|
||||
)
|
||||
self.assertEqual(
|
||||
nap_terminal["vehicle_state"]["actuator_state"]["passenger"]
|
||||
["position"]["backrest_recline"]["position_pct"],
|
||||
81,
|
||||
)
|
||||
|
||||
def test_remote_mode_api_rejects_unknown_mode(self) -> None:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"mode_id": "not_a_real_mode",
|
||||
"execution_target": "dry_run",
|
||||
"source": "android",
|
||||
"source_device_id": "n7-cockpit-test",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as caught:
|
||||
self.opener.open(request, timeout=3)
|
||||
self.assertEqual(caught.exception.code, 404)
|
||||
|
||||
def test_device_bearer_token_is_scoped_to_remote_mode_api(self) -> None:
|
||||
device = urllib.request.build_opener()
|
||||
modes = urllib.request.Request(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/modes",
|
||||
headers={"Authorization": "Bearer test-device-api-token"},
|
||||
)
|
||||
with device.open(modes, timeout=3) as response:
|
||||
catalog = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(catalog["vehicle_id"], "denza_n7")
|
||||
|
||||
runtime = urllib.request.Request(
|
||||
f"{self.base_url}/api/runtime",
|
||||
headers={"Authorization": "Bearer test-device-api-token"},
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as caught:
|
||||
device.open(runtime, timeout=3)
|
||||
self.assertEqual(caught.exception.code, 401)
|
||||
|
||||
bad_token = urllib.request.Request(
|
||||
f"{self.base_url}/api/v1/vehicles/denza_n7/modes",
|
||||
headers={
|
||||
"Authorization": "Bearer wrong-device-token",
|
||||
"X-Forwarded-For": "192.0.2.10",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as caught:
|
||||
device.open(bad_token, timeout=3)
|
||||
self.assertEqual(caught.exception.code, 401)
|
||||
|
||||
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.js?v=extension-18", timeout=3
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-67", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-69", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user