优化后端和前端

This commit is contained in:
minghanchen
2026-08-24 14:15:53 +08:00
parent 8d05815773
commit 33c92d1352
27 changed files with 2328 additions and 234 deletions
+136 -8
View File
@@ -332,18 +332,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-101", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-103", 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-9", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=sync-layout-41", timeout=3
f"{self.base_url}/console-v2.js?v=sync-layout-43", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=sync-layout-40", timeout=3
f"{self.base_url}/console-v2.css?v=sync-layout-41", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
@@ -378,12 +378,12 @@ 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-101"', html)
self.assertIn('src="/app.js?v=dual-seat-103"', html)
self.assertIn('href="/styles.css?v=dual-seat-16"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=sync-layout-41"', html)
self.assertIn('href="/console-v2.css?v=sync-layout-40"', html)
self.assertIn('src="/console-v2.js?v=sync-layout-43"', html)
self.assertIn('href="/console-v2.css?v=sync-layout-41"', html)
self.assertIn('<body class="console-v2">', html)
self.assertIn('id="v2BootScreen"', html)
self.assertIn('正在加载车机控制与实时状态', html)
@@ -612,7 +612,7 @@ operations:
self.assertIn('client_type: "web_console"', app_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v202"', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v204"', service_worker)
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
self.assertIn(".then(() => self.skipWaiting())", service_worker)
self.assertNotIn("const cached = await caches.match(request)", service_worker)
@@ -852,7 +852,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-101", timeout=3
f"{self.base_url}/app.js?v=dual-seat-103", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -1314,6 +1314,134 @@ operations:
self.assertEqual(result["provider_used"], "local_rules_fallback")
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
def test_two_stage_http_api_returns_local_commands_before_cloud_supplement(
self,
) -> None:
original_api_key = self.service.openai.api_key
self.service.openai.api_key = "two-stage-http-test-key"
preview_request = urllib.request.Request(
f"{self.base_url}/api/requests/preview",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "空调温度设置为24度,播放音乐",
"provider": "openai",
"conversation_id": "conv-e2e-two-stage-http",
"execution_mode": "dry_run",
"reasoning_effort": "medium",
"client_type": "web_console",
}
).encode("utf-8"),
)
model_output = {
"detected_domains": ["ambient_light"],
"requests": [
{
"domain": "ambient_light",
"capability": "ambient_light.set_power",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "state", "value": "on"}],
"reason": "补充播放场景氛围灯。",
}
],
"ambiguities": [],
"decision_summary": ["仅补充本地未解析片段。"],
"confidence": 0.9,
}
try:
with patch.object(
self.service.openai,
"interpret",
side_effect=AssertionError("preview must not invoke GPT"),
):
with self.opener.open(preview_request, timeout=3) as response:
preview = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 201)
local_operation = next(
operation
for config in preview["function_configs"].values()
for operation in config["spec"]["operations"]
)
self.assertEqual(local_operation["parse_source"], "local_rule")
self.assertTrue(preview["supplement_required"])
supplement_request = urllib.request.Request(
f"{self.base_url}/api/requests/{preview['request_id']}/supplement",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"provider": "openai",
"reasoning_effort": "medium",
"client_type": "web_console",
}
).encode("utf-8"),
)
with patch.object(
self.service.openai,
"interpret",
return_value=model_output,
) as interpret:
with self.opener.open(supplement_request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 201)
self.assertEqual(interpret.call_args.args[0], "播放音乐")
finally:
self.service.openai.api_key = original_api_key
self.assertEqual(result["phase"], "two_stage_complete")
sources = {
operation["capability"]: operation["parse_source"]
for config in result["function_configs"].values()
for operation in config["spec"]["operations"]
}
self.assertEqual(sources["climate.set_temperature"], "local_rule")
self.assertEqual(sources["ambient_light.set_power"], "cloud_supplement")
def test_preview_streams_validated_commands_before_terminal_result(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests/preview",
method="POST",
headers={
"Content-Type": "application/json",
"Accept": "application/x-ndjson",
},
data=json.dumps(
{
"utterance": "空调24度、风量2档",
"provider": "local",
"conversation_id": "conv-e2e-command-stream",
"execution_mode": "dry_run",
"client_type": "web_console",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
events = [
json.loads(line)
for line in response.read().decode("utf-8").splitlines()
if line.strip()
]
self.assertEqual(
response.headers.get_content_type(),
"application/x-ndjson",
)
self.assertEqual(
[event["type"] for event in events],
["validated_commands", "result"],
)
validated = events[0]["payload"]
self.assertEqual(validated["status"], "processing")
self.assertIsNone(validated["execution_report"])
self.assertTrue(validated["command_plans"])
self.assertEqual(events[1]["payload"]["status"], "succeeded")
def test_frontend_suite_covers_every_physical_seat_capability(self) -> None:
suite_path = (
REPOSITORY_ROOT
+351
View File
@@ -0,0 +1,351 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from cognitive_agent.application.native_can_intent import (
parse_exact_native_can_command,
)
from cognitive_agent.application.native_can_runtime import (
NativeCanCompileError,
NativeCanRuntime,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
HARDWARE_DISABLED_ENV = {
"COGNITIVE_AGENT_CH1_HIL_ENABLED": "false",
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
}
class Can1SemanticWhitelistTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary_directory.cleanup)
temporary_root = Path(self.temporary_directory.name)
memory_path = temporary_root / "driver-seat-memory.json"
memory_path.write_text(
json.dumps(
{
"version": 1,
"positions": {
str(slot): {
"payload_15": "15 00 00 00 00 00 00 00",
"payload_16": "16 00 00 00 00 00 00 00",
}
for slot in (1, 2, 3)
},
}
),
encoding="utf-8",
)
self.environment = patch.dict(
os.environ,
{
**HARDWARE_DISABLED_ENV,
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(memory_path),
},
clear=False,
)
self.environment.start()
self.addCleanup(self.environment.stop)
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
self.service = SeatDemoService(REPOSITORY_ROOT)
self.service.artifact_root = temporary_root / "artifacts"
@staticmethod
def _valid_exact_commands() -> list[str]:
commands = [f"door rear {state}" for state in ("lock", "unlock")]
commands.extend(
f"window {target} {motion}"
for target in (
"all",
"front",
"rear",
"front-left",
"front-right",
"rear-left",
"rear-right",
)
for motion in ("open", "close", "stop")
)
commands.extend(
f"window {target} position {position}"
for target in (
"front-left",
"front-right",
"rear-left",
"rear-right",
)
for position in (0, 100)
)
commands.extend(("ac on", "ac off"))
commands.extend(f"ac fan {level}" for level in range(1, 8))
commands.extend(
f"ac mode {mode}"
for mode in (
"face",
"face-feet",
"feet",
"feet-defrost",
"defrost",
"face-feet-defrost",
"face-defrost",
)
)
commands.extend(
f"ac temp {target} {temperature}"
for target in ("driver", "passenger", "rear")
for temperature in (18, 32)
)
commands.extend(("ac control manual", "ac control auto"))
commands.extend(
f"outlet {target} percent {horizontal} {vertical}"
for target in (
"driver-left",
"driver-right",
"passenger-left",
"passenger-right",
)
for horizontal, vertical in ((0, 0), (100, 100))
)
commands.extend(
(
"outlet driver-left position -25 -40",
"outlet driver-right position 30 30",
"outlet passenger-left position -10 -40",
"outlet passenger-right position 5 30",
)
)
commands.extend(
f"seat driver {feature} {level}"
for feature in ("vent", "heat")
for level in ("off", "low", "high")
)
commands.append("seat driver massage off")
commands.extend(
f"seat driver massage strength {strength}"
for strength in ("weak", "medium", "strong")
)
commands.extend(
f"seat driver massage mode {mode}"
for mode in ("wave", "pulse", "stretch", "slack", "relax")
)
commands.extend(
(
"seat driver horizontal forward 100",
"seat driver horizontal backward 2000",
"seat driver horizontal stop",
"seat driver height up 100",
"seat driver height down 2000",
"seat driver height stop",
"seat driver backrest forward 100",
"seat driver backrest backward 2000",
"seat driver backrest stop",
"seat driver basin up 100",
"seat driver basin down 2000",
"seat driver basin stop",
)
)
commands.extend(f"seat driver position {slot}" for slot in (1, 2, 3))
commands.extend(
f"seat driver memory recall {slot}" for slot in (1, 2, 3)
)
commands.extend(f"light indoor {state}" for state in ("on", "off"))
commands.extend(
f"light reading all {state}" for state in ("on", "off")
)
commands.extend(f"light ambient {state}" for state in ("on", "off"))
commands.extend(f"light ambient color {value}" for value in (1, 127))
commands.extend(
f"light ambient brightness {value}" for value in (1, 5)
)
commands.extend(
f"shade {motion}" for motion in ("open", "close", "half", "stop")
)
commands.extend(
f"shade position {position}" for position in (0, 1, 50, 99, 100)
)
return commands
def _compile_exact(self, command: str) -> dict:
actions = parse_exact_native_can_command(command)
self.assertEqual(len(actions), 1, command)
action = actions[0]
config = self.runtime.build_function_config(
action["domain"],
actions,
"req-can-one-whitelist",
"local_rules",
)
return self.runtime.compile(config)
@staticmethod
def _debug_commands(result: dict) -> list[str]:
return [
command["signals"]["debug_command"]
for plan in result.get("command_plans", {}).values()
for command in plan["spec"]["commands"]
]
def test_every_documented_exact_command_parses_and_compiles(self) -> None:
commands = self._valid_exact_commands()
self.assertEqual(len(commands), 119)
for command in commands:
with self.subTest(command=command):
plan = self._compile_exact(command)
self.assertEqual(
[
item["signals"]["debug_command"]
for item in plan["spec"]["commands"]
],
[command],
)
def test_out_of_whitelist_exact_values_are_rejected(self) -> None:
commands = (
"door rear open",
"window vehicle open",
"window all position 50",
"window front-left position -1",
"window front-left position 101",
"ac fan 0",
"ac fan 8",
"ac mode auto",
"ac temp driver 17",
"ac temp driver 33",
"ac temp driver 24.5",
"ac control off",
"outlet driver-left percent -1 50",
"outlet driver-left percent 101 50",
"seat driver vent medium",
"seat driver horizontal forward 99",
"seat driver horizontal forward 2001",
"seat driver horizontal stop 500",
"seat driver position 0",
"seat driver position 4",
"light ambient color 0",
"light ambient color 128",
"light ambient brightness 0",
"light ambient brightness 6",
"shade position -1",
"shade position 101",
)
for command in commands:
with self.subTest(command=command):
self.assertEqual(parse_exact_native_can_command(command), [])
def test_calibrated_outlet_values_must_fit_the_selected_outlet(self) -> None:
action = parse_exact_native_can_command(
"outlet driver-left position -26 -40"
)[0]
config = self.runtime.build_function_config(
action["domain"],
[action],
"req-can-one-calibration",
"local_rules",
)
with self.assertRaisesRegex(
NativeCanCompileError, "horizontal_value exceeds the calibrated range"
):
self.runtime.compile(config)
def test_representative_chinese_commands_cover_every_whitelist_family(self) -> None:
cases = (
("后排车门上锁", ["door rear lock"]),
("后排门锁解锁", ["door rear unlock"]),
("打开全部车窗", ["window all open"]),
("关闭前排车窗", ["window front close"]),
("停止后排车窗", ["window rear stop"]),
("把副驾车窗开到35%", ["window front-right position 35"]),
("打开空调", ["ac on"]),
("关闭空调", ["ac off"]),
("风量调到7档", ["ac fan 7"]),
("空调出风模式设为吹脸", ["ac mode face"]),
("空调出风模式设为吹面和吹脚", ["ac mode face-feet"]),
("空调出风模式设为吹脚", ["ac mode feet"]),
("空调出风模式设为吹脚除霜", ["ac mode feet-defrost"]),
("空调出风模式设为除霜", ["ac mode defrost"]),
("空调出风模式设为吹脸和吹脚除霜", ["ac mode face-feet-defrost"]),
("空调出风模式设为吹脸除霜", ["ac mode face-defrost"]),
("主驾空调温度设为18度", ["ac temp driver 18"]),
("副驾空调温度设为32度", ["ac temp passenger 32"]),
("后排空调温度设为24度", ["ac temp rear 24"]),
("空调设置为自动控制", ["ac control auto"]),
("空调设置为手动控制", ["ac control manual"]),
("主驾左风口水平25%上下75%", ["outlet driver-left percent 25 75"]),
("副驾右风口左右25%垂直75%", ["outlet passenger-right percent 25 75"]),
("主驾左风口标定坐标左右-25上下-40", ["outlet driver-left position -25 -40"]),
("打开主驾座椅通风", ["seat driver vent low"]),
("关闭主驾座椅加热", ["seat driver heat off"]),
("主驾座椅加热高档", ["seat driver heat high"]),
("关闭主驾座椅按摩", ["seat driver massage off"]),
("主驾座椅按摩强度调为中等", ["seat driver massage strength medium"]),
("主驾座椅按摩模式设为放松", ["seat driver massage mode relax"]),
("主驾座椅向前移动500毫秒", ["seat driver horizontal forward 500"]),
("主驾座椅高度向下调500毫秒", ["seat driver height down 500"]),
("主驾座椅靠背后仰800毫秒", ["seat driver backrest backward 800"]),
("把主驾座椅坐盆抬高300毫秒", ["seat driver basin up 300"]),
("调用主驾座椅记忆位置2", ["seat driver position 2"]),
("打开室内灯", ["light indoor on"]),
("关闭全部阅读灯", ["light reading all off"]),
("打开氛围灯", ["light ambient on"]),
("关闭氛围灯", ["light ambient off"]),
("把氛围灯颜色编号调到70", ["light ambient color 70"]),
("氛围灯亮度调整为3档", ["light ambient brightness 3"]),
("打开遮阳帘", ["shade open"]),
("关闭遮阳帘", ["shade close"]),
("遮阳帘半开", ["shade half"]),
("停止遮阳帘", ["shade stop"]),
("遮阳帘开到20%", ["shade position 20"]),
("遮阳帘打开30%", ["shade position 30"]),
)
for index, (utterance, expected) in enumerate(cases):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-can-one-natural-{index}",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(self._debug_commands(result), expected)
rear_context = self.service.conversations.snapshot(
"conv-can-one-natural-18"
)
self.assertEqual(
rear_context["domain_devices"]["climate"]["zones"]["rear_row"][
"temperature_c"
],
24,
)
def test_fractional_values_are_not_silently_truncated(self) -> None:
utterances = (
"空调温度设为24.5度",
"风量设为2.5档",
"氛围灯颜色编号调到70.5",
"氛围灯亮度调到3.5档",
)
for index, utterance in enumerate(utterances):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-can-one-fraction-{index}",
execution_mode="dry_run",
)
self.assertEqual(self._debug_commands(result), [])
if __name__ == "__main__":
unittest.main()
+38
View File
@@ -154,6 +154,44 @@ class Ch1DefaultTargetTests(unittest.TestCase):
"front_right",
)
def test_driver_and_passenger_window_aliases_map_to_the_correct_side(self) -> None:
cases = (
("打开主驾车窗", "front_left", "window.set_motion", {"motion": "open"}),
("打开主驾驶的车窗", "front_left", "window.set_motion", {"motion": "open"}),
("打开副驾车窗", "front_right", "window.set_motion", {"motion": "open"}),
("打开副驾驶的车窗", "front_right", "window.set_motion", {"motion": "open"}),
("把副驾车窗开到35%", "front_right", "window.set_position", {"position_pct": 35}),
)
for index, (utterance, target, capability, parameters) in enumerate(cases):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-window-occupant-alias-{index}",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(set(result["function_configs"]), {"native_window"})
operation = result["function_configs"]["native_window"]["spec"][
"operations"
][0]
self.assertEqual(operation["target"]["zone"], target)
self.assertEqual(operation["capability"], capability)
self.assertEqual(operation["parameters"], parameters)
def test_window_target_does_not_leak_from_a_later_seat_subject(self) -> None:
parsed = merge_native_can_intent({}, "打开副驾车窗和主驾座椅通风")
windows = [
action
for action in parsed["native_can_actions"]
if action["capability"] == "window.set_motion"
]
self.assertEqual(len(windows), 1)
self.assertEqual(windows[0]["target"], "front_right")
self.assertEqual(windows[0]["parameters"], {"motion": "open"})
def test_group_or_non_driver_seat_scope_is_not_treated_as_unscoped(self) -> None:
for index, utterance in enumerate(
("打开前排座椅加热", "打开后排座椅加热", "打开所有座椅加热")
+9 -1
View File
@@ -211,9 +211,17 @@ class DriverSeatNativeCanTests(unittest.TestCase):
{"axis": "height", "direction": "up", "duration_ms": 500},
),
"主驾座椅调整到记忆位置3": (
"seat.recall_driver_memory_native",
"seat.set_driver_position_native",
{"slot": 3},
),
"调用主驾座椅记忆位置2": (
"seat.set_driver_position_native",
{"slot": 2},
),
"恢复主驾座椅到二号记忆位置": (
"seat.set_driver_position_native",
{"slot": 2},
),
}
for utterance, expected in cases.items():
with self.subTest(utterance=utterance):
+132 -1
View File
@@ -27,9 +27,13 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
class _PublishResult:
rc = 0
mid = 17
def is_published(self) -> bool:
return True
def wait_for_publish(self, timeout: float) -> bool:
return timeout > 0
raise AssertionError("control publish must never block for acknowledgement")
class _FakeMqttClient:
@@ -571,11 +575,14 @@ class MqttCanTransportTests(unittest.TestCase):
self.assertEqual(payload["data"], "10 00 00 00 00 00 00 00")
self.assertEqual(payload["repeat"], 1)
self.assertEqual(payload["intervalMs"], 0)
self.assertFalse(payload["expectResponse"])
self.assertEqual(payload["responseTimeoutMs"], 100)
publication = bus.published_messages[0]
self.assertEqual(publication["topic"], topic)
self.assertEqual(publication["qos"], qos)
self.assertEqual(publication["retain"], retain)
self.assertEqual(publication["delivery_status"], "published")
self.assertEqual(publication["message_id"], 17)
self.assertEqual(publication["payload"], payload)
self.assertTrue(
{
@@ -606,6 +613,40 @@ class MqttCanTransportTests(unittest.TestCase):
self.assertEqual(len(self.client.published), 2)
self.assertEqual(len(bus.published_messages), 2)
def test_publish_returns_queued_without_waiting_for_broker_ack(self) -> None:
class QueuedPublishResult:
rc = 0
mid = 29
@staticmethod
def is_published() -> bool:
return False
@staticmethod
def wait_for_publish(timeout: float) -> bool:
raise AssertionError(
f"wait_for_publish({timeout}) must not be called"
)
with patch.object(
self.client,
"publish",
return_value=QueuedPublishResult(),
):
publication = self.transport.publish(
CanFrame(
arbitration_id=0x431,
data=bytes.fromhex("10 00 00 00 00 00 00 00"),
channel=1,
is_fd=True,
bitrate_switch=True,
),
command="shade position 20",
)
self.assertEqual(publication["delivery_status"], "queued")
self.assertEqual(publication["message_id"], 29)
def test_can1_whitelist_publishes_rear_lock_candidate_with_debug_command(self) -> None:
bus = self.transport.bus(channel=1)
bus.set_debug_command("door rear lock")
@@ -717,6 +758,96 @@ class MqttCanTransportTests(unittest.TestCase):
)
)
def test_wireless_fixed_controls_and_shade_preset_never_wait_for_state(
self,
) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
service._vehicle_mqtt_transport = self.transport
service.vehicle_mqtt_settings = SimpleNamespace(feedback_timeout_ms=1_500)
runtime = NativeCanRuntime(REPOSITORY_ROOT)
actions_by_domain = {
"climate": [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target": "driver",
"operation": "set",
"parameters": {"temperature_c": 24},
},
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target": "vehicle",
"operation": "set",
"parameters": {"level": 2},
},
],
"roof": [
{
"domain": "roof",
"capability": "roof.set_shade_motion",
"target": "vehicle",
"operation": "set",
"parameters": {"motion": "open"},
}
],
"ambient_light": [
{
"domain": "ambient_light",
"capability": "ambient_light.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
],
}
configs = {
f"native_{domain}": runtime.build_function_config(
domain,
actions,
f"req-wireless-no-wait-{domain}",
"local_rules",
)
for domain, actions in actions_by_domain.items()
}
plans = {key: runtime.compile(config) for key, config in configs.items()}
with (
patch.object(
self.transport,
"_receive_after",
side_effect=AssertionError(
"wireless fixed commands must not wait on can/state"
),
) as receive_after,
patch("cognitive_agent.application.native_can_runtime.time.sleep"),
):
reports = service._execute_orchestrated_reports(
configs,
plans,
set(configs),
{},
hardware_request=True,
simulated_hardware_request=False,
simulation_fault="normal",
control_transport="wireless_mqtt",
)
receive_after.assert_not_called()
self.assertEqual(len(self.client.published), 10)
self.assertTrue(
all(
report["spec"]["status"] == "succeeded"
for report in reports.values()
)
)
self.assertTrue(
all(
json.loads(payload)["expectResponse"] is False
for _topic, payload, _qos, _retain in self.client.published
)
)
def test_model_added_nonwhitelisted_action_does_not_block_whitelisted_publish(
self,
) -> None:
@@ -261,6 +261,7 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"capability": "seat.set_backrest_recline",
"target": {"zone": "front_right"},
"parameters": {"position_pct": 36, "angle_deg": 120},
"parse_source": "local_rule",
},
]
}
@@ -272,6 +273,7 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 24},
"parse_source": "cloud_supplement",
}
]
}
@@ -300,11 +302,13 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"category": "副驾座椅靠背调节",
"command": "seat passenger backrest position 36",
"frame": "0x6A0 · C7 <SEQ> 64 02 68 01 00 00",
"parseSource": "local_rule",
},
{
"channel": "CAN1",
"category": "空调温度",
"command": "ac temp driver 24",
"parseSource": "cloud_supplement",
},
]
+192
View File
@@ -0,0 +1,192 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from cognitive_agent.application.seat_service import SeatDemoService
ROOT = Path(__file__).resolve().parents[2]
def _service(tmp_path: Path) -> SeatDemoService:
service = SeatDemoService(ROOT)
service.artifact_root = tmp_path / "artifacts"
service.openai.api_key = "offline-two-stage-test-key"
return service
def _operations(result: dict) -> list[dict]:
return [
operation
for config in result.get("function_configs", {}).values()
for operation in config.get("spec", {}).get("operations", [])
]
def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path) -> None:
service = _service(tmp_path)
validated_events: list[dict] = []
with patch.object(
service.openai,
"interpret",
side_effect=AssertionError("preview must not call hosted parsing"),
) as interpret:
result = service.preview_native_can(
"空调温度设置为24度,风量设置为2档,播放音乐",
provider="openai",
conversation_id="conv-two-stage-local-preview",
execution_mode="dry_run",
on_commands_validated=validated_events.append,
)
interpret.assert_not_called()
assert result["phase"] == "deterministic_preview"
assert result["status"] == "succeeded"
assert result["supplement_required"] is True
assert result["unresolved_clauses"] == ["播放音乐"]
assert [item["capability"] for item in _operations(result)] == [
"climate.set_temperature",
"climate.set_fan_level",
]
assert {item["parse_source"] for item in _operations(result)} == {
"local_rule"
}
assert len(validated_events) == 1
assert validated_events[0]["phase"] == "deterministic_commands_validated"
assert validated_events[0]["status"] == "processing"
assert validated_events[0]["execution_report"] is None
assert _operations(validated_events[0]) == _operations(result)
assert validated_events[0]["command_plans"]
def test_fully_local_cabin_request_streams_all_commands_without_supplement(
tmp_path: Path,
) -> None:
service = _service(tmp_path)
validated_events: list[dict] = []
with patch.object(
service.openai,
"interpret",
side_effect=AssertionError("fully local request must not call GPT"),
):
result = service.preview_native_can(
"空调24度、风量2档,遮阳帘打开30%,打开氛围灯",
provider="openai",
conversation_id="conv-two-stage-all-local",
execution_mode="dry_run",
on_commands_validated=validated_events.append,
)
operations = _operations(result)
assert result["supplement_required"] is False
assert [item["capability"] for item in operations] == [
"climate.set_temperature",
"climate.set_fan_level",
"roof.set_shade_position",
"ambient_light.set_power",
]
assert operations[2]["parameters"]["position_pct"] == 30
assert {item["parse_source"] for item in operations} == {"local_rule"}
assert len(validated_events) == 1
assert _operations(validated_events[0]) == operations
def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control(
tmp_path: Path,
) -> None:
service = _service(tmp_path)
preview = service.preview_native_can(
"观影时空调温度设置为24度并播放音乐",
provider="openai",
conversation_id="conv-two-stage-deduplicate",
execution_mode="dry_run",
)
assert [item["capability"] for item in _operations(preview)] == [
"climate.set_temperature"
]
model_output = {
"detected_domains": ["climate", "ambient_light"],
"requests": [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "driver",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "temperature_c", "value": 24}],
"reason": "模型重复给出了本地温度动作。",
},
{
"domain": "ambient_light",
"capability": "ambient_light.set_power",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "state", "value": "on"}],
"reason": "模型补充观影氛围灯。",
},
],
"ambiguities": [],
"decision_summary": ["仅补充本地规则未覆盖的观影含义。"],
"confidence": 0.9,
}
with patch.object(
service.openai,
"interpret",
return_value=model_output,
) as interpret:
result = service.supplement_native_can(
preview["request_id"],
provider="openai",
)
assert interpret.call_args.args[0] == "观影时空调温度设置为24度并播放音乐"
operations = _operations(result)
assert [item["capability"] for item in operations].count(
"climate.set_temperature"
) == 1
assert [item["capability"] for item in operations].count(
"ambient_light.set_power"
) == 1
assert {
item["capability"]: item["parse_source"] for item in operations
} == {
"climate.set_temperature": "local_rule",
"ambient_light.set_power": "cloud_supplement",
}
cloud_results = (
result["phase_execution_reports"]["cloud_supplement"] or {}
).get("spec", {}).get("results", [])
assert len(cloud_results) == 1
def test_frontend_two_stage_contract_and_source_badges() -> None:
app = (ROOT / "apps" / "web-console" / "app.js").read_text(
encoding="utf-8"
)
console = (ROOT / "apps" / "web-console" / "console-v2.js").read_text(
encoding="utf-8"
)
styles = (ROOT / "apps" / "web-console" / "console-v2.css").read_text(
encoding="utf-8"
)
server = (ROOT / "src" / "cognitive_agent" / "server.py").read_text(
encoding="utf-8"
)
assert '"/api/requests/preview"' in app
assert "`/api/requests/${preview.request_id}/supplement`" in app
assert "renderResult(preview);" in app
assert 'Accept: "application/x-ndjson"' in app
assert '"cognitive-agent:validated-can-commands"' in app
assert "本地规则命令已执行;云端补充失败" in app
assert 'class="v2-can-command-source"' in console
assert 'item.parseSource === "local_rule" ? "本地规则" : "云端模型"' in console
assert '"cognitive-agent:validated-can-commands"' in console
assert '.v2-can-command-source[data-parse-source="local_rule"]' in styles
assert 'path not in {"/api/requests", "/api/requests/preview"}' in server
assert '"type": "validated_commands"' in server