完善模式卡片、NFC及多设备控制流程
This commit is contained in:
@@ -289,6 +289,247 @@ operations:
|
||||
self.service.mode_profiles = original_registry
|
||||
self.service.artifact_root = original_artifact_root
|
||||
|
||||
def test_office_mode_generation_and_card_execution_reuse_saved_flow(self) -> None:
|
||||
original_registry = self.service.mode_profiles
|
||||
original_artifact_root = self.service.artifact_root
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
instruction = (
|
||||
"空调温度设置为25度,风速设置为1档,"
|
||||
"启动闪烁灯,颜色设置为红色"
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
preset_root = base / "presets"
|
||||
user_root = base / "users"
|
||||
preset = preset_root / "vehicles" / "denza_n7" / "office.yaml"
|
||||
preset.parent.mkdir(parents=True)
|
||||
preset.write_text(
|
||||
"""
|
||||
id: office
|
||||
name: 办公模式
|
||||
icon: briefcase
|
||||
scope:
|
||||
vehicle_ids: [denza_n7]
|
||||
operations:
|
||||
- domain: climate
|
||||
capability: climate.set_temperature
|
||||
target_scope: driver
|
||||
parameters:
|
||||
temperature_c: 24
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=base / "office-mode-ir.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:3",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到红色",
|
||||
"action_names": [
|
||||
"切换到红色",
|
||||
"切换为红色",
|
||||
"设置为红色",
|
||||
],
|
||||
"risk": "low",
|
||||
"slot": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.service.mode_profiles = ModeProfileRegistry(
|
||||
preset_root,
|
||||
user_root=user_root,
|
||||
)
|
||||
self.service.artifact_root = base / "runs"
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
try:
|
||||
generate_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/preview",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": instruction,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-office-mode-generate",
|
||||
"vehicle_id": "denza_n7",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"client_type": "mode_editor_compile",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(generate_request, timeout=3) as response:
|
||||
generated = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
save_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/mode-profiles/office",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"name": "办公模式",
|
||||
"instruction": instruction,
|
||||
"source_request_id": generated["request_id"],
|
||||
"vehicle_id": "denza_n7",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(save_request, timeout=3) as response:
|
||||
saved = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
execute_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/mode-profiles/office/execute",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"conversation_id": "conv-office-mode-execute",
|
||||
"vehicle_id": "denza_n7",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"client_type": "web_console_mode_card",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with patch.object(
|
||||
self.service,
|
||||
"_interpret",
|
||||
side_effect=AssertionError(
|
||||
"saved mode card must not reinterpret natural language"
|
||||
),
|
||||
), self.opener.open(execute_request, timeout=3) as response:
|
||||
executed = json.loads(response.read().decode("utf-8"))
|
||||
finally:
|
||||
self.service.mode_profiles = original_registry
|
||||
self.service.artifact_root = original_artifact_root
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
generated_commands = [
|
||||
(
|
||||
command.get("signals", {}).get("debug_command")
|
||||
or (
|
||||
f"infrared emit {command.get('signals', {}).get('slot')}"
|
||||
if command.get("message") == "infrared.emit"
|
||||
else None
|
||||
)
|
||||
)
|
||||
for plan in generated.get("command_plans", {}).values()
|
||||
for command in plan.get("spec", {}).get("commands", [])
|
||||
]
|
||||
self.assertEqual(
|
||||
generated_commands,
|
||||
[
|
||||
"ac temp driver 25",
|
||||
"ac fan 1",
|
||||
"infrared emit 1",
|
||||
"infrared emit 3",
|
||||
],
|
||||
)
|
||||
self.assertEqual(saved["mode_profile"]["command_flow"]["execution_steps"], [])
|
||||
self.assertEqual(executed["status"], "succeeded")
|
||||
self.assertEqual(executed["utterance"], "办公模式")
|
||||
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
|
||||
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in executed["infrared_actions"]],
|
||||
[1, 3],
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["parse_source"] == "saved_mode_command_flow"
|
||||
for item in executed["infrared_actions"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_saved_mode_accepts_prefixed_self_extension_flow_key(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "prefixed-mode-ir.sqlite3"),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"slot": 1,
|
||||
}
|
||||
]
|
||||
)
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.extension = extension
|
||||
handler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"card": {"action": "present", "uid": "2CC8B103"}
|
||||
}
|
||||
)
|
||||
profile = SimpleNamespace(
|
||||
command_flow={
|
||||
"function_configs": {
|
||||
"local_self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"capability": "infrared.emit",
|
||||
"target": {"device": "tony-light-001"},
|
||||
"parameters": {"action": "打开", "slot": 1},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
entries = handler._saved_mode_infrared_entries(profile)
|
||||
|
||||
self.assertEqual([item["capability"]["slot"] for item in entries], [1])
|
||||
self.assertEqual(
|
||||
entries[0]["capability"]["parse_source"],
|
||||
"saved_mode_command_flow",
|
||||
)
|
||||
|
||||
def test_head_unit_display_sync_tracks_tasks_and_usb_catalog(self) -> None:
|
||||
task_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
@@ -344,14 +585,14 @@ 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-107", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-109", 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=delivery-state-49", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=delivery-state-50", timeout=3
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -394,11 +635,11 @@ 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-107"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-109"', 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=delivery-state-49"', html)
|
||||
self.assertIn('src="/console-v2.js?v=delivery-state-50"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-43"', html)
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
@@ -641,7 +882,8 @@ operations:
|
||||
)
|
||||
self.assertIn("const stageHasLayout = () =>", app_script)
|
||||
self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script)
|
||||
self.assertIn('client_type: "web_console"', app_script)
|
||||
self.assertIn('clientType = "web_console"', app_script)
|
||||
self.assertIn("client_type: clientType", app_script)
|
||||
self.assertIn("function executionVerificationStatus(result = {})", app_script)
|
||||
self.assertIn('"submitted_unverified"', app_script)
|
||||
self.assertIn("指令已提交,等待状态确认", app_script)
|
||||
@@ -649,8 +891,8 @@ 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-v218"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-107"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v220"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-109"', 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)
|
||||
@@ -890,7 +1132,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-107", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-109", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
|
||||
@@ -621,7 +621,7 @@ operations:
|
||||
if item["id"] == "office"
|
||||
)
|
||||
self.assertEqual(persisted["name"], "专注办公模式")
|
||||
with self.assertRaisesRegex(ValueError, "重新点击执行"):
|
||||
with self.assertRaisesRegex(ValueError, "生成指令"):
|
||||
self.service.save_mode_profile_card(
|
||||
"office",
|
||||
"专注办公模式",
|
||||
@@ -630,6 +630,181 @@ operations:
|
||||
"denza_n7",
|
||||
)
|
||||
|
||||
def test_saved_mode_card_executes_without_reinterpreting_instruction(self) -> None:
|
||||
mode_root = Path(self.temporary_directory.name) / "direct-mode-presets"
|
||||
user_root = Path(self.temporary_directory.name) / "direct-mode-user"
|
||||
preset = mode_root / "vehicles" / "denza_n7" / "office.yaml"
|
||||
preset.parent.mkdir(parents=True)
|
||||
preset.write_text(
|
||||
"""
|
||||
id: office
|
||||
name: 办公模式
|
||||
icon: briefcase
|
||||
scope:
|
||||
vehicle_ids: [denza_n7]
|
||||
operations:
|
||||
- domain: climate
|
||||
capability: climate.set_temperature
|
||||
target_scope: driver
|
||||
parameters:
|
||||
temperature_c: 24
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.service.mode_profiles = ModeProfileRegistry(
|
||||
mode_root, user_root=user_root
|
||||
)
|
||||
instruction = "空调温度设置为25度,风速设置为1档"
|
||||
generated = self.service.preview_native_can(
|
||||
instruction,
|
||||
provider="local",
|
||||
conversation_id="conv-generate-office-card",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
saved = self.service.save_mode_profile_card(
|
||||
"office",
|
||||
"办公模式",
|
||||
instruction,
|
||||
generated["request_id"],
|
||||
"denza_n7",
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
self.service,
|
||||
"_interpret",
|
||||
side_effect=AssertionError("saved mode must not reinterpret natural language"),
|
||||
):
|
||||
executed = self.service.execute_mode_profile_card(
|
||||
"office",
|
||||
conversation_id="conv-execute-office-card",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
|
||||
operations = [
|
||||
operation
|
||||
for config in executed.get("function_configs", {}).values()
|
||||
for operation in config.get("spec", {}).get("operations", [])
|
||||
]
|
||||
self.assertEqual(executed["status"], "succeeded")
|
||||
self.assertEqual(executed["utterance"], "办公模式")
|
||||
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
|
||||
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
operation["capability"],
|
||||
operation["target"]["zone"],
|
||||
operation["parameters"],
|
||||
operation["parse_source"],
|
||||
)
|
||||
for operation in operations
|
||||
],
|
||||
[
|
||||
(
|
||||
"climate.set_temperature",
|
||||
"driver",
|
||||
{"temperature_c": 25},
|
||||
"saved_mode_command_flow",
|
||||
),
|
||||
(
|
||||
"climate.set_fan_level",
|
||||
"vehicle",
|
||||
{"level": 1},
|
||||
"saved_mode_command_flow",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
saved["mode_profile"]["command_flow"]["execution_steps"], []
|
||||
)
|
||||
|
||||
def test_saved_mode_card_replays_front_right_dynamic_target_as_passenger(
|
||||
self,
|
||||
) -> None:
|
||||
mode_root = Path(self.temporary_directory.name) / "dynamic-mode-presets"
|
||||
user_root = Path(self.temporary_directory.name) / "dynamic-mode-user"
|
||||
self.service.mode_profiles = ModeProfileRegistry(
|
||||
mode_root,
|
||||
user_root=user_root,
|
||||
)
|
||||
self.service.mode_profiles.save_mode(
|
||||
"小憩模式",
|
||||
[
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.start_dynamic_mode",
|
||||
"target_scope": "front_right",
|
||||
"parameters": {"mode_id": 2},
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_power",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"state": "on"},
|
||||
},
|
||||
{
|
||||
"domain": "roof",
|
||||
"capability": "roof.set_shade_motion",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"motion": "open"},
|
||||
},
|
||||
],
|
||||
mode_id="nap",
|
||||
vehicle_id="denza_n7",
|
||||
instruction="打开摇篮模式,开启空调",
|
||||
command_flow={
|
||||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||||
"source_request_id": "req-saved-dynamic-mode",
|
||||
"plans": {
|
||||
"local_seat": {
|
||||
"spec": {
|
||||
"commands": [
|
||||
{
|
||||
"message": "Seat_Dynamic_Mode_Virtual",
|
||||
"signals": {"Dynamic_Mode_ID": 2},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
self.service,
|
||||
"_interpret",
|
||||
side_effect=AssertionError("saved mode must not reinterpret natural language"),
|
||||
):
|
||||
executed = self.service.execute_mode_profile_card(
|
||||
"nap",
|
||||
conversation_id="conv-execute-saved-dynamic-card",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
|
||||
self.assertEqual(executed["status"], "succeeded")
|
||||
self.assertEqual(executed["utterance"], "小憩模式")
|
||||
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
|
||||
self.assertFalse(
|
||||
[item for item in executed.get("validation", []) if item["severity"] == "error"]
|
||||
)
|
||||
dynamic_request = next(
|
||||
item
|
||||
for item in executed["model_requests"]
|
||||
if item["capability"] == "seat.start_dynamic_mode"
|
||||
)
|
||||
self.assertEqual(dynamic_request["target_scope"], "passenger")
|
||||
self.assertTrue(executed["command_plans"])
|
||||
replayed_capabilities = {
|
||||
operation["capability"]
|
||||
for config in executed["function_configs"].values()
|
||||
for operation in config["spec"]["operations"]
|
||||
}
|
||||
self.assertIn("roof.set_shade_motion", replayed_capabilities)
|
||||
|
||||
def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None:
|
||||
mode_root = Path(self.temporary_directory.name) / "modes"
|
||||
self.service.mode_profiles = ModeProfileRegistry(mode_root)
|
||||
|
||||
@@ -139,4 +139,4 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
|
||||
assert 'frameLabel: "GSCU1B TCP"' in console
|
||||
assert "已解析控制指令" in console
|
||||
assert 'li[data-can-channel="IR"]' in styles
|
||||
assert "console-v2.js?v=delivery-state-49" in html
|
||||
assert "console-v2.js?v=delivery-state-50" in html
|
||||
|
||||
@@ -437,7 +437,19 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
|
||||
assert 'source: "published"' in app
|
||||
assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app
|
||||
assert "function_configs: functionConfigs" in app
|
||||
assert "execution_steps: executionSteps" in app
|
||||
assert "execution_steps: []" in app
|
||||
assert "requestNaturalLanguageControl(instruction" in app
|
||||
assert 'executionMode: "dry_run"' in app
|
||||
assert 'clientType: "mode_editor_compile"' in app
|
||||
assert "previewOnly: true" in app
|
||||
assert "function executeSavedModeProfile(mode)" in app
|
||||
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
|
||||
assert '$("#utterance").value = mode.name || mode.id;' in app
|
||||
assert 'const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";' in app
|
||||
assert "`${result.mode_profile.name}执行完成`" in app
|
||||
|
||||
assert "生成指令" in html
|
||||
assert ">执行</span></button>" not in html
|
||||
|
||||
assert ".mode-editor-command-card" in styles
|
||||
assert ".mode-editor-command-meta" in styles
|
||||
|
||||
@@ -970,7 +970,7 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
["failed"],
|
||||
)
|
||||
|
||||
def test_submitted_emit_without_ack_continues_complete_infrared_plan(self) -> None:
|
||||
def test_power_on_then_white_waits_800ms_and_continues_without_ack(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
catalog = [
|
||||
{
|
||||
@@ -984,7 +984,7 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
}
|
||||
for capability_id, action, slot in (
|
||||
("light-on", "打开", 1),
|
||||
("light-green", "切换到绿色", 4),
|
||||
("light-white", "切换到白色", 6),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1009,16 +1009,18 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
"execute_gateway",
|
||||
side_effect=submit_without_ack,
|
||||
) as execute_gateway,
|
||||
patch("cognitive_agent.application.self_extension.time.sleep"),
|
||||
patch(
|
||||
"cognitive_agent.application.self_extension.time.sleep"
|
||||
) as sleep,
|
||||
):
|
||||
result = service.execute_peripheral_plan_request(
|
||||
"开启闪光灯,颜色设置为绿色",
|
||||
"启动闪烁灯,颜色设置为白色",
|
||||
{
|
||||
"provider_used": "infrared_local_rules",
|
||||
"summary": "打开闪光灯并切换为绿色",
|
||||
"summary": "启动闪烁灯并切换为白色",
|
||||
"steps": [
|
||||
{"kind": "peripheral", "capability_id": "light-on"},
|
||||
{"kind": "peripheral", "capability_id": "light-green"},
|
||||
{"kind": "peripheral", "capability_id": "light-white"},
|
||||
],
|
||||
},
|
||||
catalog,
|
||||
@@ -1027,11 +1029,12 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(execute_gateway.call_count, 2)
|
||||
sleep.assert_called_once_with(0.8)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["sequence"]["emit_count"], 2)
|
||||
self.assertEqual(
|
||||
[item["telemetry"]["slot"] for item in result["operation_results"]],
|
||||
[1, 4],
|
||||
[1, 6],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
|
||||
@@ -187,6 +187,7 @@ def test_frontend_two_stage_contract_and_source_badges() -> None:
|
||||
assert "本地规则命令已执行;云端补充失败" in app
|
||||
assert 'class="v2-can-command-source"' in console
|
||||
assert 'item.parseSource === "direct_control" ? "设备控件"' in console
|
||||
assert 'item.parseSource === "saved_mode_command_flow" ? "模式卡片"' in console
|
||||
assert 'item.parseSource === "local_rule" ? "本地规则" : "云端模型"' in console
|
||||
assert 'item.deliveryLabel' in console
|
||||
assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console
|
||||
|
||||
Reference in New Issue
Block a user