941 lines
32 KiB
Python
941 lines
32 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
from copy import deepcopy
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock
|
||
|
||
import pytest
|
||
|
||
from cognitive_agent.application.assistant_capabilities import (
|
||
AssistantCapabilityCatalog,
|
||
)
|
||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||
from cognitive_agent.application.n7_seat_preset import N7_SEAT_PRESET_CAPABILITIES
|
||
from cognitive_agent.application.mode_scene_ingress import (
|
||
ModeSceneIngressConsumer,
|
||
ModeSceneIngressError,
|
||
parse_mode_scene_ingress_payload,
|
||
)
|
||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||
|
||
|
||
def scene_card(*, temperature: int = 24) -> dict[str, object]:
|
||
return {
|
||
"id": "n7-reading",
|
||
"label": "N7 阅读模式",
|
||
"name": "N7 阅读模式",
|
||
"background_time": "evening",
|
||
"description": "由车端同步的阅读场景。",
|
||
"icon": "book-open",
|
||
"aliases": ["N7 阅读模式", "车端阅读"],
|
||
"origin": "user",
|
||
"deletable": True,
|
||
"behavior": "static",
|
||
"domains": ["climate"],
|
||
"operation_count": 1,
|
||
"scope": {"common": False, "vehicle_ids": ["denza_n7"]},
|
||
"profile_revision": f"remote-{temperature}",
|
||
"actions": [
|
||
{
|
||
"type": "climate_temperature",
|
||
"at_ms": 250,
|
||
"domain": "climate",
|
||
"capability": "climate.set_temperature",
|
||
"target_scope": "passenger",
|
||
"operation": "set",
|
||
"parameters": {"temperature_c": temperature},
|
||
"zone": "passenger",
|
||
"celsius": temperature,
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
def cinema_scene_card() -> dict[str, object]:
|
||
"""Representative N7 card containing local and head-unit-only actions."""
|
||
|
||
actions: list[dict[str, object]] = [
|
||
*[
|
||
{
|
||
"type": "window_position",
|
||
"at_ms": 0,
|
||
"domain": "window",
|
||
"capability": "window.set_position",
|
||
"target_scope": target,
|
||
"operation": "set",
|
||
"parameters": {"position_pct": 0},
|
||
}
|
||
for target in ("front_left", "front_right", "rear_left", "rear_right")
|
||
],
|
||
{
|
||
"type": "roof_shade_position",
|
||
"at_ms": 0,
|
||
"domain": "roof",
|
||
"capability": "roof.set_shade_position",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"position_pct": 0},
|
||
},
|
||
{
|
||
"type": "seat_preset",
|
||
"at_ms": 0,
|
||
"domain": "local_vehicle",
|
||
"capability": "local_scene.seat_preset",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"targets_pulse": [246, 112, 405, 342]},
|
||
},
|
||
{
|
||
"type": "seat_ventilation",
|
||
"at_ms": 0,
|
||
"domain": "seat",
|
||
"capability": "seat.set_ventilation",
|
||
"target_scope": "passenger",
|
||
"operation": "set",
|
||
"parameters": {"state": "off"},
|
||
},
|
||
{
|
||
"type": "climate_power",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_power",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"state": "on"},
|
||
},
|
||
{
|
||
"type": "climate_fan",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_fan_level",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"level": 2},
|
||
},
|
||
{
|
||
"type": "climate_outlet_mode",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_outlet_mode",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"mode": "face"},
|
||
},
|
||
{
|
||
"type": "climate_cycle_mode",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_cycle_mode",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"mode": "internal"},
|
||
},
|
||
{
|
||
"type": "climate_control_mode",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_control_mode",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"mode": "manual"},
|
||
},
|
||
{
|
||
"type": "climate_temperature",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_temperature",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"temperature_c": 25},
|
||
},
|
||
{
|
||
"type": "interior_light_reading_power",
|
||
"at_ms": 0,
|
||
"domain": "interior_light",
|
||
"capability": "interior_light.set_reading_power",
|
||
"target_scope": "all",
|
||
"operation": "set",
|
||
"parameters": {"state": "off"},
|
||
},
|
||
{
|
||
"type": "ambient_light_power",
|
||
"at_ms": 0,
|
||
"domain": "ambient_light",
|
||
"capability": "ambient_light.set_power",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"state": "on"},
|
||
},
|
||
{
|
||
"type": "ambient_light_color",
|
||
"at_ms": 0,
|
||
"domain": "ambient_light",
|
||
"capability": "ambient_light.set_color_index",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"color_index": 36},
|
||
},
|
||
{
|
||
"type": "ambient_light_brightness",
|
||
"at_ms": 0,
|
||
"domain": "ambient_light",
|
||
"capability": "ambient_light.set_brightness_level",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"brightness_level": 4},
|
||
},
|
||
]
|
||
return {
|
||
"id": "cinema",
|
||
"label": "Cinema mode",
|
||
"name": "Cinema mode",
|
||
"background_time": "night",
|
||
"description": "Representative N7 mixed-domain scene.",
|
||
"icon": "bookmark-plus",
|
||
"aliases": ["Cinema mode"],
|
||
"origin": "headunit",
|
||
"deletable": True,
|
||
"behavior": "static",
|
||
"domains": list(dict.fromkeys(str(item["domain"]) for item in actions)),
|
||
"operation_count": len(actions),
|
||
"scope": {"common": False, "vehicle_ids": ["denza_n7"]},
|
||
"actions": actions,
|
||
"profile_revision": "cinema-test-revision",
|
||
}
|
||
|
||
|
||
def cinema_peripheral_catalog() -> list[dict[str, object]]:
|
||
def capability(
|
||
identity: str,
|
||
device_id: str,
|
||
device_name: str,
|
||
action: str,
|
||
slot: int,
|
||
) -> dict[str, object]:
|
||
return {
|
||
"id": identity,
|
||
"status": "learned",
|
||
"enabled": True,
|
||
"device_id": device_id,
|
||
"device_name": device_name,
|
||
"device_aliases": [device_name],
|
||
"object_names": [device_name],
|
||
"card_uid": "2CC8B103" if "flash" in device_id else "1A3AB103",
|
||
"action": action,
|
||
"aliases": [action],
|
||
"action_names": [action],
|
||
"risk": "low",
|
||
"slot": slot,
|
||
}
|
||
|
||
return [
|
||
capability("flash-on", "flash-light", "闪烁灯", "打开", 1),
|
||
capability("flash-blue", "flash-light", "闪烁灯", "切换为蓝色", 3),
|
||
capability("flash-red", "flash-light", "闪烁灯", "切换为红色", 4),
|
||
capability("strip-on", "light-strip", "灯带", "打开", 21),
|
||
capability("strip-blue", "light-strip", "灯带", "切换为蓝色", 23),
|
||
capability("strip-red", "light-strip", "灯带", "切换为红色", 24),
|
||
]
|
||
|
||
|
||
def cinema_video_launch_action() -> dict[str, object]:
|
||
return {
|
||
"type": "android_app_launch",
|
||
"at_ms": 3000,
|
||
"domain": "android",
|
||
"capability": "android.launch_app",
|
||
"target_scope": "headunit",
|
||
"operation": "launch",
|
||
"parameters": {
|
||
"activity_name": "com.byd.videoplay.activity.WelComeActivity",
|
||
"package_name": "com.byd.videoplay",
|
||
},
|
||
}
|
||
|
||
|
||
def wait_until(predicate, timeout: float = 1.5) -> bool:
|
||
deadline = time.monotonic() + timeout
|
||
while time.monotonic() < deadline:
|
||
if predicate():
|
||
return True
|
||
time.sleep(0.01)
|
||
return bool(predicate())
|
||
|
||
|
||
def configured_service(tmp_path: Path) -> SeatDemoService:
|
||
service = SeatDemoService(REPOSITORY_ROOT)
|
||
service.mode_profiles = ModeProfileRegistry(
|
||
tmp_path / "presets",
|
||
user_root=tmp_path / "profiles",
|
||
)
|
||
service.artifact_root = tmp_path / "runs"
|
||
return service
|
||
|
||
|
||
def test_parser_uses_exact_single_card_contract_and_rejects_duplicate_keys() -> None:
|
||
scene = scene_card()
|
||
parsed = parse_mode_scene_ingress_payload(
|
||
json.dumps(scene, ensure_ascii=False).encode("utf-8")
|
||
)
|
||
assert parsed == scene
|
||
|
||
duplicate = (
|
||
'{"id":"one","id":"two","label":"x","name":"x",'
|
||
'"background_time":"noon","description":"",'
|
||
'"icon":"x","aliases":[],"origin":"user",'
|
||
'"deletable":true,"behavior":"static","domains":["x"],'
|
||
'"operation_count":1,"scope":{},"profile_revision":"x",'
|
||
'"actions":[]}'
|
||
)
|
||
with pytest.raises(ModeSceneIngressError, match="duplicate key"):
|
||
parse_mode_scene_ingress_payload(duplicate)
|
||
|
||
|
||
def test_three_identical_frames_are_imported_once() -> None:
|
||
imports: list[dict[str, object]] = []
|
||
consumer = ModeSceneIngressConsumer(
|
||
lambda scene: imports.append(dict(scene)) or {"status": "imported"},
|
||
deduplication_window_s=1.0,
|
||
)
|
||
payload = json.dumps(scene_card(), ensure_ascii=False).encode("utf-8")
|
||
consumer.start()
|
||
try:
|
||
for _ in range(3):
|
||
consumer.submit(payload)
|
||
assert wait_until(lambda: consumer.snapshot()["processed_count"] == 3)
|
||
assert imports == [scene_card()]
|
||
status = consumer.snapshot()
|
||
assert status["imported_count"] == 1
|
||
assert status["duplicate_count"] == 2
|
||
assert status["rejected_count"] == 0
|
||
finally:
|
||
consumer.close()
|
||
|
||
|
||
def test_failed_import_does_not_suppress_identical_retry() -> None:
|
||
importer = MagicMock(
|
||
side_effect=[
|
||
RuntimeError("temporary persistence failure"),
|
||
{"status": "imported"},
|
||
]
|
||
)
|
||
consumer = ModeSceneIngressConsumer(
|
||
importer,
|
||
deduplication_window_s=1.0,
|
||
)
|
||
payload = json.dumps(scene_card(), ensure_ascii=False).encode("utf-8")
|
||
consumer.start()
|
||
try:
|
||
consumer.submit(payload)
|
||
assert wait_until(lambda: consumer.snapshot()["processed_count"] == 1)
|
||
first_status = consumer.snapshot()
|
||
assert first_status["rejected_count"] == 1
|
||
assert first_status["imported_count"] == 0
|
||
|
||
consumer.submit(payload)
|
||
assert wait_until(lambda: consumer.snapshot()["processed_count"] == 2)
|
||
retry_status = consumer.snapshot()
|
||
assert retry_status["rejected_count"] == 1
|
||
assert retry_status["imported_count"] == 1
|
||
assert retry_status["duplicate_count"] == 0
|
||
assert importer.call_count == 2
|
||
|
||
consumer.submit(payload)
|
||
assert wait_until(lambda: consumer.snapshot()["processed_count"] == 3)
|
||
final_status = consumer.snapshot()
|
||
assert final_status["duplicate_count"] == 1
|
||
assert importer.call_count == 2
|
||
finally:
|
||
consumer.close()
|
||
|
||
|
||
def test_import_upserts_yaml_refreshes_catalog_and_does_not_echo(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
service.configure_mode_scene_catalog(tmp_path / "scenes.json")
|
||
outbound = MagicMock()
|
||
service._vehicle_mqtt_transport.publish_mode_scene = outbound
|
||
|
||
first = service.import_n7_mode_scene(scene_card(temperature=24))
|
||
updated = service.import_n7_mode_scene(scene_card(temperature=26))
|
||
|
||
assert first["scene_id"] == "n7-reading"
|
||
assert updated["scene_id"] == "n7-reading"
|
||
profiles = service.mode_profiles.public_modes("denza_n7")
|
||
assert len(profiles) == 1
|
||
profile = profiles[0]
|
||
assert profile["origin"] == "n7_mqtt"
|
||
assert profile["source_topic"] == "from_n7/scenes"
|
||
assert profile["upstream_origin"] == "user"
|
||
assert profile["operations"][0]["parameters"] == {"temperature_c": 26}
|
||
|
||
catalog = service.mode_scene_catalog("denza_n7")
|
||
assert catalog["scene_count"] == 1
|
||
card = catalog["scenes"][0]
|
||
assert card["origin"] == "n7_mqtt"
|
||
assert card["source_topic"] == "from_n7/scenes"
|
||
assert card["actions"] == scene_card(temperature=26)["actions"]
|
||
assert card["actions"][0]["at_ms"] == 250
|
||
assert outbound.call_count == 0
|
||
|
||
yaml_path = Path(service.mode_profiles.get_profile("n7-reading", "denza_n7").source)
|
||
yaml_text = yaml_path.read_text(encoding="utf-8")
|
||
assert "sync_origin: from_n7/scenes" in yaml_text
|
||
assert "upstream_origin: user" in yaml_text
|
||
|
||
|
||
def test_later_n7_cinema_mode_inherits_base_and_only_opens_external_lights(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
|
||
base = cinema_scene_card()
|
||
base_action_count = len(base["actions"])
|
||
base_result = service.import_n7_mode_scene(base)
|
||
assert "cinema_augmentation" not in base_result
|
||
|
||
variant = cinema_scene_card()
|
||
variant.update(
|
||
id="blue-cinema",
|
||
label="蓝色影院模式",
|
||
name="蓝色影院模式",
|
||
aliases=["蓝色观影模式"],
|
||
description="在车机影院模式基础上增加蓝色外设灯光。",
|
||
domains=["climate"],
|
||
operation_count=1,
|
||
actions=[
|
||
{
|
||
"type": "climate_temperature",
|
||
"at_ms": 0,
|
||
"domain": "climate",
|
||
"capability": "climate.set_temperature",
|
||
"target_scope": "vehicle",
|
||
"operation": "set",
|
||
"parameters": {"temperature_c": 23},
|
||
}
|
||
],
|
||
profile_revision="blue-cinema-revision",
|
||
)
|
||
|
||
imported = service.import_n7_mode_scene(variant)
|
||
augmentation = imported["cinema_augmentation"]
|
||
assert augmentation == {
|
||
"base_scene_id": "cinema",
|
||
"base_action_count": base_action_count,
|
||
"incoming_action_count": 1,
|
||
"peripheral_action_count": 2,
|
||
"merged_action_count": base_action_count + 2,
|
||
}
|
||
|
||
profile = service.get_mode_profile_card("blue-cinema", "denza_n7")
|
||
source_scene = profile.command_flow["source_scene"]
|
||
actions = source_scene["actions"]
|
||
assert source_scene["operation_count"] == base_action_count + 2
|
||
assert "self_extension" in source_scene["domains"]
|
||
temperatures = [
|
||
action["parameters"]["temperature_c"]
|
||
for action in actions
|
||
if action["capability"] == "climate.set_temperature"
|
||
]
|
||
assert temperatures == [23]
|
||
peripheral_actions = [
|
||
action
|
||
for action in actions
|
||
if action["capability"] == "infrared.emit"
|
||
]
|
||
assert [
|
||
(
|
||
action["target_scope"],
|
||
action["parameters"]["action"],
|
||
action["parameters"]["slot"],
|
||
)
|
||
for action in peripheral_actions
|
||
] == [
|
||
("flash-light", "打开", 1),
|
||
("light-strip", "打开", 21),
|
||
]
|
||
|
||
repeated = service.import_n7_mode_scene(variant)
|
||
assert (
|
||
repeated["cinema_augmentation"]["merged_action_count"]
|
||
== base_action_count + 2
|
||
)
|
||
reloaded = service.get_mode_profile_card("blue-cinema", "denza_n7")
|
||
assert (
|
||
len(reloaded.command_flow["source_scene"]["actions"])
|
||
== base_action_count + 2
|
||
)
|
||
|
||
|
||
def test_n7_cinema_variant_is_not_augmented_before_base_arrives(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
|
||
variant = cinema_scene_card()
|
||
variant.update(
|
||
id="family-movie",
|
||
name="家庭影院模式",
|
||
label="家庭影院模式",
|
||
profile_revision="family-movie-revision",
|
||
)
|
||
|
||
imported = service.import_n7_mode_scene(variant)
|
||
|
||
assert "cinema_augmentation" not in imported
|
||
profile = service.get_mode_profile_card("family-movie", "denza_n7")
|
||
assert not any(
|
||
operation.capability == "infrared.emit"
|
||
for operation in profile.operations
|
||
)
|
||
|
||
|
||
def test_created_romantic_cinema_persists_and_transfers_n7_video_launch(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
base = cinema_scene_card()
|
||
base["actions"] = [*base["actions"], cinema_video_launch_action()]
|
||
base["operation_count"] = len(base["actions"])
|
||
base["domains"] = [*base["domains"], "android"]
|
||
service.import_n7_mode_scene(base)
|
||
|
||
request_id = "req-romantic-cinema-video"
|
||
instruction = "进入浪漫观影模式"
|
||
result_dir = service.artifact_root / request_id
|
||
result_dir.mkdir(parents=True)
|
||
(result_dir / "result.json").write_text(
|
||
json.dumps(
|
||
{
|
||
"request_id": request_id,
|
||
"status": "succeeded",
|
||
"execution_mode": "dry_run",
|
||
"control_transport": "wireless_mqtt",
|
||
"utterance": instruction,
|
||
"vehicle": {"id": "denza_n7"},
|
||
"function_configs": {
|
||
"climate": {
|
||
"spec": {
|
||
"domain": "climate",
|
||
"operations": [
|
||
{
|
||
"id": "vehicle-climate-power",
|
||
"capability": "climate.set_power",
|
||
"target": {"zone": "vehicle"},
|
||
"parameters": {"state": "on"},
|
||
}
|
||
],
|
||
}
|
||
}
|
||
},
|
||
"command_plans": {
|
||
"climate": {
|
||
"spec": {
|
||
"commands": [
|
||
{
|
||
"operation_ref": "vehicle-climate-power",
|
||
"message": "climate.set_power",
|
||
}
|
||
]
|
||
}
|
||
}
|
||
},
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
saved = service.create_mode_profile_card(
|
||
"浪漫观影模式",
|
||
instruction,
|
||
request_id,
|
||
"denza_n7",
|
||
)
|
||
profile = service.get_mode_profile_card(
|
||
str(saved["mode_profile"]["id"]),
|
||
"denza_n7",
|
||
)
|
||
|
||
assert [
|
||
operation.capability for operation in profile.operations
|
||
] == ["climate.set_power", "android.launch_app"]
|
||
assert profile.command_flow["n7_transfer_actions"] == [
|
||
cinema_video_launch_action()
|
||
]
|
||
local_requests, transfer_actions = service._mode_requests_and_n7_transfers(
|
||
profile
|
||
)
|
||
assert [request["capability"] for request in local_requests] == [
|
||
"climate.set_power"
|
||
]
|
||
assert transfer_actions == [cinema_video_launch_action()]
|
||
|
||
exported = next(
|
||
scene
|
||
for scene in service.mode_scene_catalog("denza_n7")["scenes"]
|
||
if scene["id"] == profile.id
|
||
)
|
||
video = next(
|
||
action
|
||
for action in exported["actions"]
|
||
if action["capability"] == "android.launch_app"
|
||
)
|
||
assert video == cinema_video_launch_action()
|
||
|
||
|
||
def test_existing_local_cinema_mode_publishes_video_action_only_in_hardware(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
base = cinema_scene_card()
|
||
base["actions"] = [*base["actions"], cinema_video_launch_action()]
|
||
base["operation_count"] = len(base["actions"])
|
||
base["domains"] = [*base["domains"], "android"]
|
||
service.import_n7_mode_scene(base)
|
||
profile = service.mode_profiles.save_mode(
|
||
"浪漫观影模式",
|
||
[
|
||
{
|
||
"domain": "android",
|
||
"capability": "android.launch_app",
|
||
"target_scope": "headunit",
|
||
"operation": "launch",
|
||
"parameters": deepcopy(cinema_video_launch_action()["parameters"]),
|
||
}
|
||
],
|
||
vehicle_id="denza_n7",
|
||
instruction="进入浪漫观影模式",
|
||
command_flow={
|
||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||
"plans": {"saved": {"spec": {"commands": [{"message": "saved"}]}}},
|
||
},
|
||
)
|
||
publisher = MagicMock(
|
||
return_value={
|
||
"topic": "transto_n7/ctl",
|
||
"qos": 1,
|
||
"retain": False,
|
||
"delivery_status": "published",
|
||
"message_id": 19,
|
||
}
|
||
)
|
||
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
|
||
|
||
preview = service.execute_mode_profile_card(
|
||
profile.id,
|
||
execution_mode="dry_run",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
publisher.assert_not_called()
|
||
assert preview["n7_transfer_candidates"] == [cinema_video_launch_action()]
|
||
|
||
executed = service.execute_mode_profile_card(
|
||
profile.id,
|
||
execution_mode="hardware",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
|
||
assert executed["n7_control_transfer"]["state"] == "published"
|
||
publisher.assert_called_once()
|
||
payload = publisher.call_args.args[0]
|
||
assert payload["scene_id"] == profile.id
|
||
assert payload["scene_name"] == "浪漫观影模式"
|
||
assert payload["operation_count"] == 1
|
||
assert payload["actions"] == [cinema_video_launch_action()]
|
||
|
||
|
||
def test_unknown_n7_action_is_forwarded_once_only_during_hardware_execution(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
unknown = scene_card()
|
||
unknown.update(
|
||
domains=["media"],
|
||
operation_count=1,
|
||
actions=[
|
||
{
|
||
"type": "media_play_favorite",
|
||
"at_ms": 0,
|
||
"domain": "media",
|
||
"capability": "media.play_favorite",
|
||
"target_scope": "vehicle",
|
||
"operation": "play",
|
||
"parameters": {"playlist": "focus"},
|
||
}
|
||
],
|
||
)
|
||
service.import_n7_mode_scene(unknown)
|
||
publisher = MagicMock(
|
||
return_value={
|
||
"topic": "transto_n7/ctl",
|
||
"qos": 1,
|
||
"retain": False,
|
||
"delivery_status": "published",
|
||
"message_id": 7,
|
||
}
|
||
)
|
||
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
|
||
|
||
dry_run = service.execute_mode_profile_card(
|
||
"n7-reading",
|
||
execution_mode="dry_run",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
assert dry_run["status"] == "capability_unavailable"
|
||
publisher.assert_not_called()
|
||
|
||
result = service.execute_mode_profile_card(
|
||
"n7-reading",
|
||
execution_mode="hardware",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
|
||
assert result["status"] == "capability_unavailable"
|
||
assert result["n7_control_transfer"]["state"] == "published"
|
||
publisher.assert_called_once()
|
||
payload = publisher.call_args.args[0]
|
||
assert payload == {
|
||
"scene_id": "n7-reading",
|
||
"scene_name": "N7 阅读模式",
|
||
"request_id": result["request_id"],
|
||
"operation_count": 1,
|
||
"actions": deepcopy(unknown["actions"]),
|
||
}
|
||
assert result["deferred_requests"][0]["transport_submitted"] is False
|
||
|
||
|
||
def test_mixed_n7_scene_compiles_every_local_action_and_preserves_transfer(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
scene = cinema_scene_card()
|
||
scene["actions"].append(cinema_video_launch_action())
|
||
scene["operation_count"] = len(scene["actions"])
|
||
service.import_n7_mode_scene(scene)
|
||
|
||
result = service.execute_mode_profile_card(
|
||
"cinema",
|
||
execution_mode="dry_run",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
|
||
assert result["status"] == "partially_succeeded"
|
||
assert not [
|
||
issue
|
||
for issue in result["validation"]
|
||
if issue.get("severity") == "error"
|
||
]
|
||
compiled_operations = [
|
||
operation
|
||
for config in result["function_configs"].values()
|
||
for operation in config["spec"]["operations"]
|
||
]
|
||
assert len(compiled_operations) == 20
|
||
positions = {
|
||
operation["capability"]: operation["parameters"]["position_pct"]
|
||
for operation in compiled_operations
|
||
if operation["capability"] in N7_SEAT_PRESET_CAPABILITIES
|
||
}
|
||
assert positions == dict(zip(N7_SEAT_PRESET_CAPABILITIES, [51, 30, 44, 53], strict=True))
|
||
hardware_plan = service.hardware_compiler.compile(result["function_configs"]["seat"])
|
||
moves = [
|
||
command["signals"] for command in hardware_plan["spec"]["commands"]
|
||
if command["message"] == "T2G_Seat_Move_Command_300"
|
||
]
|
||
assert [(move["Motor_Selector"], move["Opcode"], move["Param_A"]) for move in moves] == [
|
||
(0, 1, 5100), (1, 1, 3000), (2, 1, 4400), (3, 1, 5300),
|
||
]
|
||
assert all(
|
||
operation.get("parse_source") == "saved_mode_command_flow"
|
||
for operation in compiled_operations
|
||
)
|
||
assert {
|
||
operation["target"]["zone"]
|
||
for operation in compiled_operations
|
||
if operation["capability"] == "window.set_position"
|
||
} == {"front_left", "front_right", "rear_left", "rear_right"}
|
||
transferred = cinema_video_launch_action()
|
||
assert result["n7_transfer_candidates"] == [transferred]
|
||
assert result["deferred_requests"] == [
|
||
{
|
||
"domain": "android",
|
||
"capability": "android.launch_app",
|
||
"target_scope": "headunit",
|
||
"desired_change": "launch",
|
||
"parameters": transferred["parameters"],
|
||
"reason": "当前 PC 未注册该能力,将由 N7 端继续处理。",
|
||
"execution_backend": "n7_vehicle_transfer",
|
||
"rejection_code": "PC_CAPABILITY_UNAVAILABLE",
|
||
"transport_submitted": False,
|
||
}
|
||
]
|
||
assert len(compiled_operations) + len(result["n7_transfer_candidates"]) == len(
|
||
scene["actions"]
|
||
) + 3
|
||
|
||
text_result = service.process(
|
||
"进入Cinema mode模式",
|
||
provider="local",
|
||
conversation_id="conv-n7-cinema-text",
|
||
execution_mode="dry_run",
|
||
vehicle_id="denza_n7",
|
||
control_transport="wireless_mqtt",
|
||
)
|
||
assert text_result["status"] == "partially_succeeded"
|
||
assert text_result["mode_profile"] is None
|
||
assert [item["id"] for item in text_result["matched_modes"]] == ["cinema"]
|
||
assert len(text_result["n7_transfer_candidates"]) == 1
|
||
assert {
|
||
operation["capability"]
|
||
for operation in text_result["function_configs"]["seat"]["spec"]["operations"]
|
||
} >= set(N7_SEAT_PRESET_CAPABILITIES)
|
||
assert service.mode_profiles.match_text(
|
||
"氛围灯设置为Cinema mode模式",
|
||
vehicle_id="denza_n7",
|
||
) == []
|
||
|
||
catalog_mode = next(
|
||
mode
|
||
for mode in AssistantCapabilityCatalog(service).snapshot("denza_n7")["modes"]
|
||
if mode["id"] == "cinema"
|
||
)
|
||
assert catalog_mode["assistant_callable"] is True
|
||
assert catalog_mode["n7_transfer_operation_ids"] == [
|
||
"android.launch_app"
|
||
]
|
||
assert set(catalog_mode["operation_ids"]) >= set(N7_SEAT_PRESET_CAPABILITIES)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("domain", "target_scope", "parameters"),
|
||
[
|
||
("media", "passenger", {"temperature_c": 24}),
|
||
("climate", "unsupported_zone", {"temperature_c": 24}),
|
||
("climate", "passenger", {"unsupported_parameter": 24}),
|
||
],
|
||
)
|
||
def test_registered_but_invalid_action_is_not_forwarded_to_n7(
|
||
tmp_path: Path,
|
||
domain: str,
|
||
target_scope: str,
|
||
parameters: dict[str, object],
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
invalid = scene_card()
|
||
invalid["actions"] = [
|
||
{
|
||
"type": "climate_temperature",
|
||
"at_ms": 0,
|
||
"domain": domain,
|
||
"capability": "climate.set_temperature",
|
||
"target_scope": target_scope,
|
||
"operation": "set",
|
||
"parameters": parameters,
|
||
}
|
||
]
|
||
service.import_n7_mode_scene(invalid)
|
||
profile = service.get_mode_profile_card("n7-reading", "denza_n7")
|
||
|
||
local_requests, transfer_actions = service._mode_requests_and_n7_transfers(profile)
|
||
|
||
assert len(local_requests) == 1
|
||
assert transfer_actions == []
|
||
|
||
|
||
def test_existing_saved_cinema_cards_adapt_on_execution_without_reimport(tmp_path: Path) -> None:
|
||
service = configured_service(tmp_path)
|
||
scene = cinema_scene_card()
|
||
scene["actions"].append(cinema_video_launch_action())
|
||
service.import_n7_mode_scene(scene)
|
||
imported = service.get_mode_profile_card("cinema", "denza_n7")
|
||
legacy = service.mode_profiles.save_mode(
|
||
"旧版浪漫影院模式", [op.to_yaml() for op in imported.operations],
|
||
vehicle_id="denza_n7", instruction="进入旧版浪漫影院模式",
|
||
command_flow={
|
||
"plans": {"saved": {"spec": {"commands": [{"message": "saved"}]}}},
|
||
"n7_transfer_actions": [scene["actions"][5], cinema_video_launch_action()],
|
||
},
|
||
)
|
||
before = {path: path.read_bytes() for path in (tmp_path / "profiles").rglob("*.yaml")}
|
||
service.mode_profiles = ModeProfileRegistry(
|
||
tmp_path / "presets", user_root=tmp_path / "profiles"
|
||
)
|
||
publisher = MagicMock(side_effect=AssertionError("preview must not publish"))
|
||
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
|
||
try:
|
||
for mode_id in ("cinema", legacy.id):
|
||
result = service.execute_mode_profile_card(
|
||
mode_id, execution_mode="dry_run", control_transport="wireless_mqtt"
|
||
)
|
||
assert result["status"] == "partially_succeeded"
|
||
assert result["n7_transfer_candidates"] == [cinema_video_launch_action()]
|
||
operations = result["function_configs"]["seat"]["spec"]["operations"]
|
||
assert [op["parameters"]["position_pct"] for op in operations
|
||
if op["capability"] in N7_SEAT_PRESET_CAPABILITIES] == [51, 30, 44, 53]
|
||
assert all(path.read_bytes() == value for path, value in before.items())
|
||
publisher.assert_not_called()
|
||
finally:
|
||
service.close()
|
||
|
||
|
||
def test_invalid_preset_blocks_the_card_instead_of_transferring_it(tmp_path: Path) -> None:
|
||
service = configured_service(tmp_path)
|
||
scene = cinema_scene_card()
|
||
scene["actions"][5]["parameters"]["targets_pulse"][0] = 9999
|
||
service.import_n7_mode_scene(scene)
|
||
publisher = MagicMock(side_effect=AssertionError("invalid motion must not publish"))
|
||
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
|
||
try:
|
||
with pytest.raises(ValueError, match="0–480"):
|
||
service.execute_mode_profile_card(
|
||
"cinema", execution_mode="hardware", control_transport="wireless_mqtt"
|
||
)
|
||
publisher.assert_not_called()
|
||
finally:
|
||
service.close()
|
||
|
||
|
||
def test_frontend_marks_n7_synchronized_cards() -> None:
|
||
script = (REPOSITORY_ROOT / "apps" / "web-console" / "app.js").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
styles = (
|
||
REPOSITORY_ROOT / "apps" / "web-console" / "styles.css"
|
||
).read_text(encoding="utf-8")
|
||
assert 'mode?.origin === "n7_mqtt"' in script
|
||
assert 'sourceBadge.textContent = "N7 同步"' in script
|
||
assert ".mode-gallery-source-badge" in styles
|
||
|
||
|
||
def test_mode_profile_sync_status_exposes_catalog_and_ingress_counters(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
service = configured_service(tmp_path)
|
||
service.configure_mode_scene_catalog(tmp_path / "scenes.json")
|
||
|
||
status = service.mode_profile_sync_status()
|
||
|
||
assert len(status["catalog_revision"]) == 16
|
||
assert status["scene_count"] == 0
|
||
assert status["catalog_state"] == "ready"
|
||
assert status["mode_scene_ingress"]["imported_count"] == 0
|
||
|
||
|
||
def test_frontend_polls_lightweight_status_and_refreshes_on_n7_import() -> None:
|
||
script = (REPOSITORY_ROOT / "apps" / "web-console" / "app.js").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
assert 'apiRequest("/api/mode-profiles/status"' in script
|
||
assert "MODE_PROFILE_SYNC_POLL_INTERVAL_MS = 1000" in script
|
||
assert 'document.visibilityState === "hidden"' in script
|
||
assert "if (catalogChanged || (imported && !sceneAlreadyVisible))" in script
|
||
assert "await loadRuntime()" in script
|
||
assert "revealModeProfile(sceneId)" in script
|
||
assert "已从 N7 同步模式卡片" in script
|
||
assert "N7 模式卡片导入失败" in script
|