添加模式卡片下发

This commit is contained in:
Miayui
2026-09-09 14:37:25 +08:00
parent 76057be044
commit 51ca12b624
10 changed files with 1169 additions and 0 deletions
+29
View File
@@ -134,6 +134,35 @@ class SeatWebApiTests(unittest.TestCase):
unauthenticated.open(f"{self.base_url}/api/runtime", timeout=3)
self.assertEqual(caught.exception.code, 401)
def test_n7_mode_scene_catalog_is_public_and_read_only(self) -> None:
unauthenticated = urllib.request.build_opener()
with unauthenticated.open(f"{self.base_url}/n7/scenes", timeout=3) as response:
catalog = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 200)
self.assertEqual(response.headers["Cache-Control"], "no-store")
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertEqual(catalog["service"], "n7-scenes")
self.assertEqual(catalog["schema_version"], 1)
self.assertEqual(catalog["vehicle_id"], "denza_n7")
self.assertGreaterEqual(catalog["scene_count"], 13)
self.assertEqual(catalog["scene_count"], len(catalog["scenes"]))
office = next(scene for scene in catalog["scenes"] if scene["id"] == "office")
self.assertIn(office["origin"], {"preset", "customized"})
self.assertTrue(office["actions"])
self.assertIn("capability", office["actions"][0])
self.assertNotIn("command_flow", json.dumps(catalog, ensure_ascii=False))
preflight = urllib.request.Request(
f"{self.base_url}/n7/scenes",
method="OPTIONS",
)
with unauthenticated.open(preflight, timeout=3) as response:
self.assertEqual(response.status, 204)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertIn("GET", response.headers["Access-Control-Allow-Methods"])
def test_user_mode_delete_endpoint_removes_only_user_profiles(self) -> None:
original_registry = self.service.mode_profiles
with tempfile.TemporaryDirectory() as directory:
+158
View File
@@ -0,0 +1,158 @@
from __future__ import annotations
import json
from pathlib import Path
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.mode_scene_catalog import (
ModeSceneCatalogPublisher,
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