Files
Cognitive_Agent/tests/unit/test_mode_scene_ingress.py
T

868 lines
29 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.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_adds_external_light_colors(
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": 4,
"merged_action_count": base_action_count + 4,
}
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 + 4
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),
("flash-light", "切换为蓝色", 3),
("light-strip", "打开", 21),
("light-strip", "切换为蓝色", 23),
]
repeated = service.import_n7_mode_scene(variant)
assert (
repeated["cinema_augmentation"]["merged_action_count"]
== base_action_count + 4
)
reloaded = service.get_mode_profile_card("blue-cinema", "denza_n7")
assert (
len(reloaded.command_flow["source_scene"]["actions"])
== base_action_count + 4
)
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()
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) == 16
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 = scene["actions"][5]
assert result["n7_transfer_candidates"] == [transferred]
assert result["deferred_requests"] == [
{
"domain": "local_vehicle",
"capability": "local_scene.seat_preset",
"target_scope": "vehicle",
"desired_change": "set",
"parameters": {"targets_pulse": [246, 112, 405, 342]},
"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"]
)
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 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"] == [
"local_scene.seat_preset"
]
@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_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