添加NFC绑定红外设备方式,融合车端设备和扩展设备统一控制
This commit is contained in:
+595
-15
@@ -33,6 +33,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
cls.extension_temporary_directory = tempfile.TemporaryDirectory()
|
||||
# Web regression tests must never inherit a developer's live bench
|
||||
# enablement, especially when local debug mode intentionally skips the
|
||||
# per-request approval token.
|
||||
@@ -50,6 +51,13 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.service = service
|
||||
cls.original_service = SeatDemoHandler.service
|
||||
SeatDemoHandler.service = service
|
||||
cls.original_extension = SeatDemoHandler.extension
|
||||
SeatDemoHandler.extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
cls.extension_temporary_directory.name, "gscu-ir-catalog.sqlite3"
|
||||
),
|
||||
)
|
||||
cls.original_auth = SeatDemoHandler.auth
|
||||
SeatDemoHandler.auth = AuthRuntime(
|
||||
AuthConfig(
|
||||
@@ -79,7 +87,9 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.server.server_close()
|
||||
cls.thread.join(timeout=2)
|
||||
SeatDemoHandler.service = cls.original_service
|
||||
SeatDemoHandler.extension = cls.original_extension
|
||||
SeatDemoHandler.auth = cls.original_auth
|
||||
cls.extension_temporary_directory.cleanup()
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
def setUp(self) -> None:
|
||||
@@ -341,19 +351,19 @@ operations:
|
||||
) as response:
|
||||
gesture_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.js?v=task-outcome-47", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=delivery-state-49", 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-41", timeout=3
|
||||
f"{self.base_url}/console-v2.css?v=delivery-state-43", timeout=3
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-37", timeout=3
|
||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-40", timeout=3
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.css?v=gscu-tcp-37", timeout=3
|
||||
f"{self.base_url}/self-extension.css?v=gscu-tcp-39", timeout=3
|
||||
) as response:
|
||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||
with self.opener.open(
|
||||
@@ -388,13 +398,13 @@ operations:
|
||||
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=task-outcome-47"', html)
|
||||
self.assertIn('href="/console-v2.css?v=sync-layout-41"', html)
|
||||
self.assertIn('src="/console-v2.js?v=delivery-state-49"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-43"', html)
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
self.assertIn('正在加载车机控制与实时状态', html)
|
||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-37"', html)
|
||||
self.assertIn('href="/self-extension.css?v=gscu-tcp-37"', html)
|
||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-40"', html)
|
||||
self.assertIn('href="/self-extension.css?v=gscu-tcp-39"', html)
|
||||
self.assertNotIn("self-calibration", html)
|
||||
self.assertNotIn("self-repair", html)
|
||||
self.assertNotIn("self-evolution", html)
|
||||
@@ -639,7 +649,7 @@ operations:
|
||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v214"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v218"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-107"', service_worker)
|
||||
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
|
||||
self.assertIn(".then(() => self.skipWaiting())", service_worker)
|
||||
@@ -1013,7 +1023,12 @@ operations:
|
||||
|
||||
def test_self_extension_gateway_uses_unified_authenticated_service(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
SeatDemoHandler.extension = SelfExtensionService(SimulationGateway())
|
||||
SeatDemoHandler.extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "gateway-auth.sqlite3"
|
||||
),
|
||||
)
|
||||
try:
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/extension/health", timeout=3
|
||||
@@ -1039,7 +1054,12 @@ operations:
|
||||
|
||||
def test_self_extension_tcp_configuration_endpoint_updates_runtime(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
extension = SelfExtensionService(SimulationGateway())
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "tcp-config.sqlite3"
|
||||
),
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1093,11 +1113,38 @@ operations:
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
structured_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/slots/5",
|
||||
method="PUT",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"device_id": "toy-light-001",
|
||||
"object_names": ["闪烁灯", "闪灯玩具"],
|
||||
"action_names": ["切换为蓝色", "设置为蓝色"],
|
||||
"source": "tcp",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(structured_request, timeout=3) as response:
|
||||
structured = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(structured["slot"]["device_id"], "toy-light-001")
|
||||
self.assertEqual(
|
||||
structured["slot"]["object_names"], ["闪烁灯", "闪灯玩具"]
|
||||
)
|
||||
self.assertEqual(
|
||||
structured["slot"]["action_names"],
|
||||
["切换为蓝色", "设置为蓝色"],
|
||||
)
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/extension/gscu/slots", timeout=3
|
||||
) as response:
|
||||
catalog = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual([item["slot"] for item in catalog["slots"]], [3, 12])
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in catalog["slots"]], [3, 5, 12]
|
||||
)
|
||||
|
||||
delete_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/slots/3",
|
||||
@@ -1117,7 +1164,7 @@ operations:
|
||||
with self.opener.open(erase_request, timeout=3) as response:
|
||||
erased = json.loads(response.read().decode("utf-8"))
|
||||
self.assertTrue(erased["catalog_cleared"])
|
||||
self.assertEqual(erased["catalog_removed_count"], 1)
|
||||
self.assertEqual(erased["catalog_removed_count"], 2)
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/extension/gscu/slots", timeout=3
|
||||
@@ -1130,7 +1177,12 @@ operations:
|
||||
def test_main_request_routes_live_nfc_peripheral_to_gscu(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
SeatDemoHandler.extension = SelfExtensionService(SimulationGateway())
|
||||
SeatDemoHandler.extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "main-nfc-route.sqlite3"
|
||||
),
|
||||
)
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
@@ -1197,10 +1249,538 @@ operations:
|
||||
self.assertEqual(operation["domain"], "self_extension")
|
||||
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
||||
|
||||
def test_main_request_combines_explicit_ir_clause_with_existing_can_parser(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "mixed-nfc-can.sqlite3"
|
||||
),
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
catalog_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/display-sync",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"peripherals": [
|
||||
{
|
||||
"id": "toy-001-light-on",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "toy-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "开启",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "toy-001-light-red",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "toy-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到红色",
|
||||
"action_names": ["切换到红色", "切换为红色", "设置为红色"],
|
||||
"risk": "low",
|
||||
"slot": 3,
|
||||
},
|
||||
]
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/x-ndjson",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "打开闪烁灯,颜色切换到红色,打开空调",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-mixed-nfc-can",
|
||||
"execution_mode": "dry_run",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with self.opener.open(catalog_request, timeout=3) as response:
|
||||
synced = json.loads(response.read().decode("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()
|
||||
]
|
||||
result = events[-1]["payload"]
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertEqual(synced["learned_count"], 2)
|
||||
self.assertEqual(events[-1]["type"], "result")
|
||||
self.assertTrue(any(item["type"] == "validated_commands" for item in events))
|
||||
validated_payloads = [
|
||||
item["payload"]
|
||||
for item in events
|
||||
if item["type"] == "validated_commands"
|
||||
]
|
||||
self.assertTrue(validated_payloads)
|
||||
self.assertTrue(
|
||||
all(
|
||||
"self_extension" in payload["function_configs"]
|
||||
for payload in validated_payloads
|
||||
)
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["orchestration_route"], "unified_can_infrared")
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in result["infrared_actions"]], [1, 3]
|
||||
)
|
||||
self.assertEqual(
|
||||
result["infrared_actions"][0]["parse_source"], "local_rule"
|
||||
)
|
||||
self.assertIn("self_extension", result["function_configs"])
|
||||
self.assertTrue(
|
||||
any(key != "self_extension" for key in result["function_configs"])
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
item.get("domain") == "self_extension"
|
||||
for item in result["operation_results"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_hardware_can_failure_keeps_infrared_in_validated_command_event(
|
||||
self,
|
||||
) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name,
|
||||
"mixed-hardware-validation.sqlite3",
|
||||
),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:4",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到绿色",
|
||||
"action_names": ["切换到绿色", "切换为绿色", "设置为绿色"],
|
||||
"risk": "low",
|
||||
"slot": 4,
|
||||
},
|
||||
]
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/x-ndjson",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": (
|
||||
"打开空调,温度设置为24度,风速调整为3档,"
|
||||
"打开遮阳帘,开启闪光灯,颜色设置为绿色"
|
||||
),
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-hardware-mixed-validation",
|
||||
"execution_mode": "hardware",
|
||||
"control_transport": "wired_can",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
|
||||
def fail_can_after_validation(*_args, **kwargs):
|
||||
callback = kwargs.get("_on_commands_validated")
|
||||
self.assertTrue(callable(callback))
|
||||
callback(
|
||||
{
|
||||
"request_id": "req-can-validation-only",
|
||||
"utterance": "打开空调,温度设置为24度,风速调整为3档,打开遮阳帘",
|
||||
"status": "processing",
|
||||
"execution_mode": "hardware",
|
||||
"control_transport": "wired_can",
|
||||
"provider_used": "local_rules",
|
||||
"decision_summary": [],
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"id": "vehicle_set_power_1",
|
||||
"capability": "climate.set_power",
|
||||
"target": {"zone": "vehicle"},
|
||||
"parameters": {"state": "on"},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_plans": {
|
||||
"native_climate": {"spec": {"commands": []}}
|
||||
},
|
||||
"operation_results": [],
|
||||
"execution_report": None,
|
||||
}
|
||||
)
|
||||
raise CanGatewayError("simulated CAN transport failure")
|
||||
|
||||
try:
|
||||
with patch.object(
|
||||
self.service,
|
||||
"process",
|
||||
side_effect=fail_can_after_validation,
|
||||
):
|
||||
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()
|
||||
]
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertEqual(events[-1]["type"], "error")
|
||||
self.assertEqual(events[-1]["error"], "can_transport_failed")
|
||||
validated = next(
|
||||
item["payload"]
|
||||
for item in events
|
||||
if item["type"] == "validated_commands"
|
||||
)
|
||||
self.assertEqual(validated["status"], "processing")
|
||||
self.assertEqual(
|
||||
set(validated["function_configs"]),
|
||||
{"native_climate", "self_extension"},
|
||||
)
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in validated["infrared_actions"]],
|
||||
[1, 4],
|
||||
)
|
||||
|
||||
def test_disabled_nfc_ir_command_does_not_discard_allowed_can_and_ir(
|
||||
self,
|
||||
) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name,
|
||||
"mixed-disabled-nfc-command.sqlite3",
|
||||
),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:5",
|
||||
"status": "learned",
|
||||
"enabled": False,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到蓝色",
|
||||
"action_names": ["切换到蓝色", "切换为蓝色", "设置为蓝色"],
|
||||
"risk": "low",
|
||||
"slot": 5,
|
||||
},
|
||||
]
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/x-ndjson",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": (
|
||||
"打开空调,温度设置为24度,风速调整为3档,"
|
||||
"开启闪光灯,颜色设置为蓝色"
|
||||
),
|
||||
"provider": "openai",
|
||||
"conversation_id": "conv-disabled-nfc-command",
|
||||
"execution_mode": "dry_run",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with patch.object(
|
||||
extension,
|
||||
"plan",
|
||||
side_effect=AssertionError(
|
||||
"a disabled exact command must not be sent to the cloud"
|
||||
),
|
||||
):
|
||||
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()
|
||||
]
|
||||
result = events[-1]["payload"]
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertEqual(events[-1]["type"], "result")
|
||||
self.assertEqual(result["status"], "partially_succeeded")
|
||||
self.assertEqual(result["orchestration_route"], "unified_can_infrared")
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in result["infrared_actions"]], [1]
|
||||
)
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in result["denied_peripheral_actions"]],
|
||||
[5],
|
||||
)
|
||||
self.assertEqual(
|
||||
result["denied_peripheral_actions"][0]["reason"],
|
||||
"nfc_command_disabled",
|
||||
)
|
||||
debug_commands = [
|
||||
command["signals"]["debug_command"]
|
||||
for plan in result["command_plans"].values()
|
||||
for command in plan["spec"].get("commands", [])
|
||||
if command.get("signals", {}).get("debug_command")
|
||||
]
|
||||
self.assertEqual(
|
||||
debug_commands,
|
||||
["ac on", "ac temp driver 24", "ac fan 3"],
|
||||
)
|
||||
infrared_slots = [
|
||||
command["signals"]["slot"]
|
||||
for command in result["command_plans"]["self_extension"]["spec"][
|
||||
"commands"
|
||||
]
|
||||
]
|
||||
self.assertEqual(infrared_slots, [1])
|
||||
|
||||
def test_generic_vehicle_light_text_never_routes_to_ir_alias(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "vehicle-light-not-ir.sqlite3"
|
||||
),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "toy-001-light-on",
|
||||
"status": "learned",
|
||||
"device_id": "toy-001",
|
||||
"device_name": "玩具 001",
|
||||
"device_aliases": ["氛围灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "开启闪灯",
|
||||
"aliases": ["打开"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
}
|
||||
]
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "打开氛围灯,空调24度",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-vehicle-light-not-ir",
|
||||
"execution_mode": "dry_run",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertNotIn("infrared_actions", result)
|
||||
self.assertNotEqual(result.get("orchestration_route"), "unified_can_infrared")
|
||||
self.assertNotIn("self_extension", result["function_configs"])
|
||||
self.assertTrue(result["function_configs"])
|
||||
|
||||
def test_mixed_preview_reports_cloud_selected_infrared_capability(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name,
|
||||
"mixed-cloud-nfc-can.sqlite3",
|
||||
),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "toy-001-light-red",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "toy-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到红色",
|
||||
"action_names": ["切换到红色", "切换为红色"],
|
||||
"slot": 3,
|
||||
}
|
||||
]
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
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": "把闪烁灯调成喜庆颜色,打开空调",
|
||||
"provider": "openai",
|
||||
"conversation_id": "conv-mixed-cloud-ir",
|
||||
"execution_mode": "dry_run",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
plan_result = {
|
||||
"model": "gpt-5.6-luna",
|
||||
"summary": "喜庆颜色对应已学习的红色动作。",
|
||||
"needs_confirmation": False,
|
||||
"steps": [
|
||||
{"kind": "peripheral", "capability_id": "toy-001-light-red"}
|
||||
],
|
||||
}
|
||||
try:
|
||||
with patch.object(extension, "plan", return_value=plan_result) as planner:
|
||||
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()
|
||||
]
|
||||
result = events[-1]["payload"]
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
planner.assert_called_once()
|
||||
self.assertEqual(result["orchestration_route"], "unified_can_infrared")
|
||||
self.assertIn("gpt:gpt-5.6-luna", result["provider_used"])
|
||||
self.assertEqual(
|
||||
result["infrared_actions"][0]["parse_source"], "cloud_model"
|
||||
)
|
||||
self.assertFalse(result["supplement_required"])
|
||||
|
||||
def test_main_request_executes_gpt_nfc_sequence_without_vehicle_can(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
extension = SelfExtensionService(SimulationGateway())
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(
|
||||
self.extension_temporary_directory.name, "main-nfc-sequence.sqlite3"
|
||||
),
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
|
||||
Reference in New Issue
Block a user