Files
Cognitive_Agent/tests/unit/test_mode_scene_catalog.py
T

406 lines
13 KiB
Python

from __future__ import annotations
import json
import threading
import time
from pathlib import Path
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.mode_scene_catalog import (
ModeSceneCatalogPublisher,
ModeSceneMqttBurstPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
def _write_office_preset(path: Path) -> None:
path.parent.mkdir(parents=True)
path.write_text(
"""
id: office
name: 办公模式
description: N7 办公场景。
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: passenger
operation: set
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
def test_publisher_tracks_create_override_and_delete(tmp_path: Path) -> None:
preset_root = tmp_path / "presets"
user_root = tmp_path / "user"
_write_office_preset(
preset_root / "vehicles" / "denza_n7" / "preset" / "office.yaml"
)
registry = ModeProfileRegistry(preset_root, user_root=user_root)
catalog_path = tmp_path / "scenses" / "demo-scenes.json"
publisher = ModeSceneCatalogPublisher(
catalog_path,
"denza_n7",
server_id="test-n7",
)
registry.add_change_listener(publisher.publish)
initial = json.loads(catalog_path.read_text(encoding="utf-8"))
assert initial["service"] == "n7-scenes"
assert initial["schema_version"] == 1
assert initial["server_id"] == "test-n7"
assert initial["scene_count"] == 1
assert initial["scenes"][0]["origin"] == "preset"
assert initial["scenes"][0]["actions"] == [
{
"type": "climate_temperature",
"at_ms": 0,
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"operation": "set",
"parameters": {"temperature_c": 24},
"zone": "passenger",
"celsius": 24,
}
]
created = registry.save_mode(
"阅读模式",
[
{
"domain": "ambient_light",
"capability": "ambient_light.set_illumination",
"target_scope": "vehicle",
"parameters": {"color_hex": "#E8F3FF", "brightness_pct": 40},
}
],
vehicle_id="denza_n7",
)
after_create = json.loads(catalog_path.read_text(encoding="utf-8"))
created_scene = next(
scene for scene in after_create["scenes"] if scene["id"] == created.id
)
assert after_create["scene_count"] == 2
assert created_scene["origin"] == "user"
assert created_scene["deletable"] is True
assert created_scene["actions"][0]["parameters"]["brightness_pct"] == 40
fan_mode = registry.save_mode(
"微风模式",
[
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target_scope": "vehicle",
"parameters": {"level": 2},
}
],
vehicle_id="denza_n7",
)
after_fan_create = json.loads(catalog_path.read_text(encoding="utf-8"))
fan_scene = next(
scene for scene in after_fan_create["scenes"] if scene["id"] == fan_mode.id
)
assert fan_scene["actions"][0]["type"] == "climate_fan"
assert fan_scene["actions"][0]["level"] == 2
registry.save_mode(
"专注办公模式",
[
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"parameters": {"temperature_c": 26},
}
],
mode_id="office",
vehicle_id="denza_n7",
)
after_override = json.loads(catalog_path.read_text(encoding="utf-8"))
office = next(scene for scene in after_override["scenes"] if scene["id"] == "office")
assert office["label"] == "专注办公模式"
assert office["origin"] == "customized"
assert office["preset_override"] is True
assert office["actions"][0]["celsius"] == 26
registry.delete_user_mode(created.id, "denza_n7")
registry.delete_user_mode(fan_mode.id, "denza_n7")
registry.delete_user_mode("office", "denza_n7")
after_delete = json.loads(catalog_path.read_text(encoding="utf-8"))
assert [scene["id"] for scene in after_delete["scenes"]] == ["office"]
assert after_delete["scenes"][0]["label"] == "办公模式"
assert after_delete["scenes"][0]["origin"] == "preset"
def test_repository_presets_are_exported_without_internal_command_flows() -> None:
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
catalog = build_mode_scene_catalog(
registry,
"denza_n7",
generated_at="2026-09-09T00:00:00Z",
)
assert catalog["scene_count"] >= 13
assert len({scene["id"] for scene in catalog["scenes"]}) == catalog["scene_count"]
by_id = {scene["id"]: scene for scene in catalog["scenes"]}
assert by_id["office"]["operation_count"] == 10
assert by_id["rocking_horse"]["behavior"] == "cyclic"
assert by_id["rocking_horse"]["timeline"]["repeat"] == "until_stopped_or_timeout"
encoded = json.dumps(catalog, ensure_ascii=False)
assert "command_flow" not in encoded
assert "source_request_id" not in encoded
def test_saved_editor_flow_projects_all_native_and_extension_operations(
tmp_path: Path,
) -> None:
registry = ModeProfileRegistry(
tmp_path / "presets",
user_root=tmp_path / "profiles",
)
operations = [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "driver",
"parameters": {"temperature_c": 24},
},
{
"domain": "interior_light",
"capability": "interior_light.set_reading_power",
"target_scope": "all",
"parameters": {"state": "on"},
},
{
"domain": "roof",
"capability": "roof.set_shade_motion",
"target_scope": "vehicle",
"parameters": {"motion": "open"},
},
{
"domain": "window",
"capability": "window.set_position",
"target_scope": "front_right",
"parameters": {"position_pct": 50},
},
]
def configured_operation(
operation_id: str,
capability: str,
zone: str,
parameters: dict[str, object],
*,
device: str,
) -> dict[str, object]:
return {
"id": operation_id,
"capability": capability,
"parameters": parameters,
"target": {"device": device, "zone": zone},
}
function_configs = {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
configured_operation(
"driver_set_temperature_1",
"climate.set_temperature",
"driver",
{"temperature_c": 24},
device="hvac",
)
],
}
},
"native_interior_light": {
"spec": {
"domain": "interior_light",
"operations": [
configured_operation(
"all_set_reading_power_1",
"interior_light.set_reading_power",
"all",
{"state": "on"},
device="reading_light",
)
],
}
},
"native_roof": {
"spec": {
"domain": "roof",
"operations": [
configured_operation(
"vehicle_set_shade_motion_1",
"roof.set_shade_motion",
"vehicle",
{"motion": "open"},
device="roof_shade",
)
],
}
},
"native_window": {
"spec": {
"domain": "window",
"operations": [
configured_operation(
"front_right_set_position_1",
"window.set_position",
"front_right",
{"position_pct": 50},
device="window",
)
],
}
},
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"id": "ir-slot:toy-light-001:2-1",
"capability": "infrared.emit",
"parameters": {"action": "打开", "slot": 2},
"target": {
"card_uid": "2CC8B103",
"device": "toy-light-001",
"name": "闪烁灯",
},
},
{
"id": "ir-slot:toy-light-001:6-2",
"capability": "infrared.emit",
"parameters": {"action": "设置为白色", "slot": 6},
"target": {
"card_uid": "2CC8B103",
"device": "toy-light-001",
"name": "闪烁灯",
},
},
],
}
},
}
registry.save_mode(
"阅读模式",
operations,
mode_id="mode-reading",
vehicle_id="denza_n7",
command_flow={"function_configs": function_configs},
)
catalog = build_mode_scene_catalog(
registry,
"denza_n7",
generated_at="2026-09-15T00:00:00Z",
)
reading = next(
scene for scene in catalog["scenes"] if scene["id"] == "mode-reading"
)
assert reading["operation_count"] == 6
assert len(reading["actions"]) == 6
assert [action["capability"] for action in reading["actions"]] == [
"climate.set_temperature",
"interior_light.set_reading_power",
"roof.set_shade_motion",
"window.set_position",
"infrared.emit",
"infrared.emit",
]
assert reading["domains"] == [
"climate",
"interior_light",
"roof",
"self_extension",
"window",
]
first_infrared, second_infrared = reading["actions"][-2:]
assert first_infrared["parameters"] == {"action": "打开", "slot": 2}
assert second_infrared["parameters"] == {"action": "设置为白色", "slot": 6}
assert first_infrared["target"] == {
"card_uid": "2CC8B103",
"device": "toy-light-001",
"name": "闪烁灯",
}
assert second_infrared["target"] == first_infrared["target"]
assert first_infrared["operation"] == "emit"
def test_mode_scene_mqtt_burst_publishes_only_one_exact_card_three_times() -> None:
publications: list[tuple[float, dict[str, object]]] = []
complete = threading.Event()
scene: dict[str, object] = {
"id": "mode-reading",
"label": "阅读模式",
"name": "阅读模式",
"background_time": "noon",
"description": "由自然语言生成的自定义模式。",
"icon": "bookmark-plus",
"aliases": ["阅读模式", "阅读"],
"origin": "user",
"deletable": True,
"behavior": "static",
"domains": ["climate"],
"operation_count": 1,
"scope": {"common": False, "vehicle_ids": ["denza_n7"]},
"profile_revision": "1234567890abcdef",
"actions": [
{
"type": "climate_temperature",
"at_ms": 0,
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"operation": "set",
"parameters": {"temperature_c": 24},
"zone": "passenger",
"celsius": 24,
}
],
}
def publish(payload: dict[str, object]) -> dict[str, object]:
publications.append((time.monotonic(), payload))
if len(publications) == 3:
complete.set()
return {"delivery_status": "published", "message_id": len(publications)}
publisher = ModeSceneMqttBurstPublisher(
publish,
topic="from_pc/scenes",
burst_window_s=0.12,
)
queued_at = time.monotonic()
try:
queued = publisher.queue(scene)
assert complete.wait(timeout=1.0)
assert queued == {
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": "mode-reading",
"burst_count": 3,
"burst_window_ms": 120,
}
assert len(publications) == 3
assert publications[-1][0] - queued_at < 0.2
assert all(payload == scene for _, payload in publications)
assert all("scenes" not in payload for _, payload in publications)
assert publisher.snapshot()["state"] == "burst_complete"
finally:
publisher.close()