Merge origin/feature/N7Web into feature/N7Web
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
This commit is contained in:
@@ -11,6 +11,7 @@ from cognitive_agent.application.mode_scene_catalog import (
|
||||
ModeSceneMqttBurstPublisher,
|
||||
build_mode_scene_catalog,
|
||||
)
|
||||
from cognitive_agent.application.mode_schedule import ModeSchedule
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
|
||||
|
||||
|
||||
@@ -153,7 +154,7 @@ def test_repository_presets_are_exported_without_internal_command_flows() -> Non
|
||||
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["office"]["operation_count"] == len(registry.get_profile("office", "denza_n7").operations)
|
||||
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)
|
||||
@@ -403,3 +404,87 @@ def test_mode_scene_mqtt_burst_publishes_only_one_exact_card_three_times() -> No
|
||||
assert publisher.snapshot()["state"] == "burst_complete"
|
||||
finally:
|
||||
publisher.close()
|
||||
|
||||
|
||||
def _schedule() -> dict:
|
||||
return ModeSchedule.from_dict({
|
||||
"schema": "cognitive-agent-mode-schedule/v1",
|
||||
"repeat_count": 2,
|
||||
"phases": [
|
||||
{"id": "warm", "name": "暖舱", "duration_ms": 1000, "operations": [
|
||||
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
|
||||
"operation": "set", "parameters": {"temperature_c": 28}},
|
||||
]},
|
||||
{"id": "settle", "name": "舒适", "duration_ms": 2000, "operations": [
|
||||
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
|
||||
"operation": "set", "parameters": {"temperature_c": 24}},
|
||||
{"domain": "seat", "capability": "seat.set_backrest_recline", "target_scope": "passenger",
|
||||
"operation": "set", "parameters": {"position_pct": 45}},
|
||||
]},
|
||||
],
|
||||
}).to_dict()
|
||||
|
||||
|
||||
def test_schedule_catalog_preserves_phases_but_never_exports_flat_executable_actions(tmp_path: Path) -> None:
|
||||
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
|
||||
schedule = _schedule()
|
||||
profile = registry.save_mode(
|
||||
"分阶段舒适模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
|
||||
schedule=ModeSchedule.from_dict(schedule),
|
||||
command_flow={"source_request_id": "req-private", "raw_can": "private-transport-bytes"},
|
||||
)
|
||||
catalog = build_mode_scene_catalog(registry, "denza_n7")
|
||||
scene = next(item for item in catalog["scenes"] if item["id"] == profile.id)
|
||||
assert scene["schedule"] == schedule
|
||||
assert scene["behavior"] == "cyclic"
|
||||
assert scene["operation_count"] == 3
|
||||
assert scene["domains"] == ["climate", "seat"]
|
||||
assert scene["actions"] == [], "legacy clients must not execute all timeline stages at once"
|
||||
assert scene["execution"] == {
|
||||
"kind": "server_schedule", "mode_id": profile.id,
|
||||
"client_actions_supported": False, "requires_authentication": True,
|
||||
}
|
||||
assert "服务端时间轴" in scene["execution_notice"]
|
||||
assert scene["preview_actions_scope"] == "single_cycle"
|
||||
preview = scene["preview_actions"]
|
||||
assert [action["phase_id"] for action in preview] == ["warm", "settle", "settle"]
|
||||
assert [action["at_ms"] for action in preview] == [0, 1000, 1000]
|
||||
assert [action["phase_duration_ms"] for action in preview] == [1000, 2000, 2000]
|
||||
assert [action["celsius"] for action in preview if action["type"] == "climate_temperature"] == [28, 24]
|
||||
assert preview[-1]["target_scope"] == "passenger"
|
||||
assert preview[-1]["parameters"] == {"position_pct": 45}
|
||||
assert ModeSchedule.from_dict(scene["schedule"]).total_duration_ms == 6000
|
||||
encoded = json.dumps(catalog)
|
||||
assert "source_request_id" not in encoded
|
||||
assert "private-transport-bytes" not in encoded
|
||||
assert "command_flow" not in encoded
|
||||
|
||||
|
||||
def test_schedule_publisher_updates_after_save_reload_duration_change_and_delete(tmp_path: Path) -> None:
|
||||
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
|
||||
publisher = ModeSceneCatalogPublisher(tmp_path / "catalog.json", "denza_n7")
|
||||
registry.add_change_listener(publisher.publish)
|
||||
assert publisher.snapshot()["scene_count"] == 0
|
||||
schedule = _schedule()
|
||||
created = registry.save_mode(
|
||||
"动态释压模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
|
||||
schedule=ModeSchedule.from_dict(schedule),
|
||||
)
|
||||
first = publisher.snapshot()
|
||||
assert first["scenes"][0]["schedule"] == schedule
|
||||
registry.reload()
|
||||
assert publisher.snapshot() == first
|
||||
schedule["phases"][1]["duration_ms"] = 5000
|
||||
registry.save_mode(
|
||||
created.name, schedule["phases"][0]["operations"], vehicle_id="denza_n7",
|
||||
mode_id=created.id, schedule=ModeSchedule.from_dict(schedule),
|
||||
)
|
||||
changed = publisher.snapshot()
|
||||
assert changed["catalog_revision"] != first["catalog_revision"]
|
||||
assert changed["scenes"][0]["profile_revision"] != first["scenes"][0]["profile_revision"]
|
||||
assert changed["scenes"][0]["schedule"]["phases"][1]["duration_ms"] == 5000
|
||||
assert changed["scenes"][0]["actions"] == []
|
||||
assert json.loads(publisher.path.read_text(encoding="utf-8")) == changed
|
||||
registry.delete_user_mode(created.id, "denza_n7")
|
||||
assert publisher.snapshot()["scene_count"] == 0
|
||||
assert publisher.snapshot()["scenes"] == []
|
||||
|
||||
Reference in New Issue
Block a user