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()