Merge origin/feature/N7Web into feature/N7Web
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
This commit is contained in:
@@ -52,6 +52,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
@@ -328,6 +329,37 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
self.assertIn("imported_count", payload["mode_scene_ingress"])
|
||||
self.assertNotIn("modes", payload)
|
||||
|
||||
def test_schedule_status_stays_authenticated_and_sanitized_beside_public_catalog(self) -> None:
|
||||
endpoint = f"{self.base_url}/api/modes/schedule/status?vehicle_id=denza_n7"
|
||||
unauthenticated = urllib.request.build_opener()
|
||||
with self.assertRaises(urllib.error.HTTPError) as caught:
|
||||
unauthenticated.open(endpoint, timeout=3)
|
||||
self.assertEqual(caught.exception.code, 401)
|
||||
state = {
|
||||
"status": "running", "vehicle_id": "denza_n7", "session_id": "conv-mode-schedule-api",
|
||||
"run_id": "schedule-private-safe-id", "phase_index": 0, "repeat_index": 0,
|
||||
"phase_count": 2, "repeat_count": 2, "executed_phases": 0,
|
||||
"elapsed_ms": 250, "total_duration_ms": 6000, "completed": False,
|
||||
"phase_results": [{"raw_can": "private-command-bytes"}],
|
||||
"hardware_client_id": "private-bench-client",
|
||||
"error": "private transport stack or secret",
|
||||
}
|
||||
with (
|
||||
patch.object(self.service.mode_schedule_runner, "status", return_value=state) as status,
|
||||
self.opener.open(endpoint, timeout=3) as response,
|
||||
):
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
status.assert_called_once_with("denza_n7")
|
||||
self.assertEqual(payload["status"], "running")
|
||||
self.assertEqual(payload["phase_count"], 2)
|
||||
self.assertEqual(payload["total_duration_ms"], 6000)
|
||||
self.assertEqual(payload["completed"], False)
|
||||
self.assertIn("后续阶段已停止", payload["error"])
|
||||
self.assertNotIn("phase_results", payload)
|
||||
self.assertNotIn("hardware_client_id", payload)
|
||||
self.assertNotIn("private-command-bytes", json.dumps(payload))
|
||||
self.assertNotIn("private transport", json.dumps(payload))
|
||||
|
||||
def test_user_mode_delete_endpoint_removes_only_user_profiles(self) -> None:
|
||||
original_registry = self.service.mode_profiles
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1353,7 +1385,7 @@ operations:
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn(
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v321-n7-dispatch"',
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v322-n7-firmware-merge"',
|
||||
service_worker,
|
||||
)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
|
||||
@@ -1363,6 +1395,9 @@ operations:
|
||||
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-145-n7-dispatch"', service_worker)
|
||||
self.assertIn('/firmware-workbench-entry.css?v=1', service_worker)
|
||||
self.assertIn('/firmware-workbench-entry.js?v=2', service_worker)
|
||||
self.assertIn('/firmware-workbench.html', service_worker)
|
||||
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
|
||||
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
||||
@@ -2601,6 +2636,15 @@ operations:
|
||||
if item["type"] == "validated_commands"
|
||||
]
|
||||
self.assertTrue(validated_payloads)
|
||||
# The local-first stage publishes known IR controls before CAN parsing;
|
||||
# subsequent evidence must include the complete combined command set.
|
||||
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
|
||||
self.assertTrue(
|
||||
all(
|
||||
"self_extension" in payload["function_configs"]
|
||||
for payload in validated_payloads
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
{"native_climate", "self_extension"}.issubset(
|
||||
@@ -2861,11 +2905,16 @@ operations:
|
||||
|
||||
self.assertEqual(events[-1]["type"], "error")
|
||||
self.assertEqual(events[-1]["error"], "can_transport_failed")
|
||||
validated = [
|
||||
validated_payloads = [
|
||||
item["payload"]
|
||||
for item in events
|
||||
if item["type"] == "validated_commands"
|
||||
][-1]
|
||||
]
|
||||
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
|
||||
validated = next(
|
||||
payload for payload in validated_payloads
|
||||
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
|
||||
)
|
||||
self.assertEqual(validated["status"], "processing")
|
||||
self.assertEqual(
|
||||
set(validated["function_configs"]),
|
||||
@@ -3238,6 +3287,31 @@ operations:
|
||||
planner.assert_called_once()
|
||||
self.assertEqual(planner.call_args.args[0]["utterance"], utterance)
|
||||
|
||||
def test_peripheral_timing_aliases_preserve_sequence_before_local_action_execution(self) -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.service = SimpleNamespace(is_mode_profile_configuration_request=lambda _text: False)
|
||||
capabilities = [
|
||||
{"id": identity, "status": "learned", "device_id": "third-party-light",
|
||||
"device_name": "第三方氛围灯", "card_uid": "1A53DD95", "action": action, "slot": slot}
|
||||
for identity, action, slot in (("on", "开启灯光", 1), ("red", "红色灯光", 2), ("off", "关闭灯光", 3))
|
||||
]
|
||||
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: capabilities)
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {
|
||||
"reader": {"online": True}, "card": {"action": "present", "uid": "1A53DD95"},
|
||||
})
|
||||
for duration in ("5s", "5S", "500ms", "500MS", "1.5s", "5秒", "半分钟"):
|
||||
with (
|
||||
self.subTest(duration=duration),
|
||||
patch.object(handler, "_execute_active_peripheral_request", return_value={"status": "succeeded"}) as planner,
|
||||
patch.object(handler, "_execute_local_peripheral_actions", side_effect=AssertionError("sequence flattened")),
|
||||
):
|
||||
result = handler._execute_request_with_peripherals("/api/requests", {
|
||||
"utterance": f"打开第三方氛围灯,红灯闪烁{duration}后,最后关闭第三方氛围灯",
|
||||
"provider": "local", "execution_mode": "dry_run",
|
||||
})
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
planner.assert_called_once()
|
||||
|
||||
def test_post_request_returns_complete_dry_run(self) -> None:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
|
||||
Reference in New Issue
Block a user