587 lines
21 KiB
Python
587 lines
21 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
|
|
|
|
|
|
class ModeProfileRegistryTests(unittest.TestCase):
|
|
def test_zero_gravity_profile_matches_alias_and_builds_requests(self) -> None:
|
|
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
|
|
|
matched = registry.match_text("副驾驶进入零重力模式", vehicle_id="denza_n7")
|
|
self.assertEqual([profile.id for profile in matched], ["zero_gravity"])
|
|
self.assertEqual(matched[0].vehicle_ids, ("denza_n7",))
|
|
|
|
requests = registry.requests_for_profiles(matched)
|
|
self.assertGreaterEqual(len(requests), 4)
|
|
self.assertEqual(requests[0]["domain"], "seat")
|
|
self.assertEqual(requests[0]["capability"], "seat.set_longitudinal_position")
|
|
self.assertEqual(requests[0]["target_scope"], "passenger")
|
|
self.assertIn(
|
|
{"name": "position_pct", "value": 100},
|
|
requests[0]["parameters"],
|
|
)
|
|
by_capability = {request["capability"]: request for request in requests}
|
|
self.assertEqual(
|
|
by_capability["seat.set_cushion_tilt"]["parameters"],
|
|
[{"name": "position_pct", "value": 100}],
|
|
)
|
|
self.assertEqual(
|
|
by_capability["seat.set_backrest_recline"]["parameters"],
|
|
[
|
|
{"name": "position_pct", "value": 81},
|
|
{"name": "angle_deg", "value": 150},
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
by_capability["seat.set_legrest_height"]["parameters"],
|
|
[
|
|
{"name": "position_pct", "value": 94},
|
|
{"name": "angle_deg", "value": 163},
|
|
],
|
|
)
|
|
|
|
public = registry.public_modes()
|
|
self.assertIn("零重力模式", {item["name"] for item in public})
|
|
self.assertNotIn(
|
|
"zero_gravity",
|
|
{profile.id for profile in registry.match_text("副驾驶进入零重力模式", vehicle_id="other_vehicle")},
|
|
)
|
|
|
|
def test_reference_modes_are_denza_specific(self) -> None:
|
|
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
|
|
|
expected_ids = {
|
|
"zero_gravity",
|
|
"cruise_sofa",
|
|
"leisure_lounger",
|
|
"nap",
|
|
"easy_entry_exit",
|
|
"baby_nap",
|
|
"office",
|
|
"meditation_space",
|
|
"electric_beach_chair",
|
|
"queen_passenger_driving",
|
|
"calf_decompression_float",
|
|
"rocking_horse",
|
|
"dynamic_cradle",
|
|
}
|
|
denza_modes = registry.public_modes(vehicle_id="denza_n7")
|
|
denza_ids = {mode["id"] for mode in denza_modes}
|
|
|
|
self.assertTrue(expected_ids.issubset(denza_ids))
|
|
self.assertTrue(
|
|
expected_ids.isdisjoint(
|
|
{mode["id"] for mode in registry.public_modes(vehicle_id="other_vehicle")}
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
registry.match_text("进入办公模式", vehicle_id="denza_n7")[0].id,
|
|
"office",
|
|
)
|
|
self.assertEqual(
|
|
registry.match_text("进入小腿解压悬浮模式", vehicle_id="denza_n7")[0].id,
|
|
"calf_decompression_float",
|
|
)
|
|
|
|
def test_dynamic_modes_preserve_exact_timelines_and_native_mode_ids(self) -> None:
|
|
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
|
profiles = {
|
|
profile.id: profile
|
|
for profile in registry.profiles()
|
|
if profile.id in {"rocking_horse", "dynamic_cradle"}
|
|
}
|
|
|
|
horse = profiles["rocking_horse"]
|
|
self.assertIsNotNone(horse.timeline)
|
|
assert horse.timeline is not None
|
|
self.assertEqual(horse.timeline.repeat, "until_stopped_or_timeout")
|
|
self.assertEqual(horse.timeline.max_runtime_ms, 600_000)
|
|
self.assertIsNone(horse.timeline.cycle_duration_ms)
|
|
self.assertEqual(
|
|
[(phase.id, phase.duration_ms) for phase in horse.timeline.phases],
|
|
[("rock_forward", None), ("rock_backward", None)],
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
|
|
320,
|
|
)
|
|
self.assertEqual(
|
|
[phase.completion for phase in horse.timeline.phases],
|
|
["all_targets_reached", "all_targets_reached"],
|
|
)
|
|
self.assertEqual([phase.timeout_ms for phase in horse.timeline.phases], [15_000, 15_000])
|
|
self.assertEqual(horse.timeline.phases[0].motion["final_approach_hall_pulses"], 8)
|
|
self.assertTrue(horse.timeline.phases[0].motion["continuous_four_axis_cruise"])
|
|
self.assertEqual(
|
|
horse.timeline.phases[0].targets["cushion_tilt"]["hall_pulse"],
|
|
250,
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.phases[1].targets["cushion_tilt"]["hall_pulse"],
|
|
130,
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.phases[0].motion["cruise_duty_percent"]["cushion_tilt"],
|
|
90,
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.phases[0].targets["longitudinal"]["hall_pulse"],
|
|
270,
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.phases[1].targets["longitudinal"]["hall_pulse"],
|
|
370,
|
|
)
|
|
self.assertEqual(
|
|
horse.timeline.phases[0].motion["cruise_duty_percent"]["longitudinal"],
|
|
90,
|
|
)
|
|
|
|
cradle = profiles["dynamic_cradle"]
|
|
self.assertIsNotNone(cradle.timeline)
|
|
assert cradle.timeline is not None
|
|
self.assertEqual(cradle.timeline.repeat, "until_stopped_or_timeout")
|
|
self.assertEqual(cradle.timeline.max_runtime_ms, 600_000)
|
|
self.assertIsNone(cradle.timeline.cycle_duration_ms)
|
|
self.assertEqual(
|
|
[(phase.id, phase.duration_ms) for phase in cradle.timeline.phases],
|
|
[("forward", None), ("return", None)],
|
|
)
|
|
self.assertEqual(
|
|
cradle.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
|
|
391,
|
|
)
|
|
self.assertEqual(
|
|
[phase.completion for phase in cradle.timeline.phases],
|
|
["all_targets_reached", "all_targets_reached"],
|
|
)
|
|
self.assertEqual(
|
|
[phase.timeout_ms for phase in cradle.timeline.phases],
|
|
[30_000, 30_000],
|
|
)
|
|
self.assertEqual(
|
|
cradle.timeline.phases[0].motion["final_approach_hall_pulses"],
|
|
12,
|
|
)
|
|
self.assertTrue(
|
|
cradle.timeline.phases[0].motion["continuous_four_axis_cruise"]
|
|
)
|
|
self.assertNotIn("clearance_lead", cradle.timeline.phases[0].motion)
|
|
|
|
requests = {
|
|
profile.id: registry.requests_for_profiles([profile])[0]
|
|
for profile in (horse, cradle)
|
|
}
|
|
self.assertEqual(
|
|
requests["rocking_horse"]["parameters"],
|
|
[{"name": "mode_id", "value": 1}],
|
|
)
|
|
self.assertEqual(
|
|
requests["dynamic_cradle"]["parameters"],
|
|
[{"name": "mode_id", "value": 2}],
|
|
)
|
|
|
|
def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None:
|
|
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
|
expected_positions = {
|
|
"baby_nap": (81, 30, 41, 0),
|
|
"calf_decompression_float": (51, 75, 0, 100),
|
|
"cruise_sofa": (51, 30, 44, 53),
|
|
"easy_entry_exit": (100, 0, 0, 0),
|
|
"electric_beach_chair": (100, 0, 100, 60),
|
|
"leisure_lounger": (51, 30, 66, 76),
|
|
"meditation_space": (100, 44, 44, 58),
|
|
"nap": (100, 100, 81, 94),
|
|
"office": (72, 22, 22, 21),
|
|
"queen_passenger_driving": (72, 38, 44, 31),
|
|
"zero_gravity": (100, 100, 81, 94),
|
|
}
|
|
|
|
profiles = {
|
|
profile.id: profile
|
|
for profile in registry.profiles()
|
|
if profile.id in expected_positions and profile.vehicle_ids == ("denza_n7",)
|
|
}
|
|
actual_positions = {}
|
|
for profile_id, profile in profiles.items():
|
|
requests = {
|
|
request["capability"]: request
|
|
for request in registry.requests_for_profiles([profile])
|
|
}
|
|
actual_positions[profile_id] = tuple(
|
|
next(
|
|
parameter["value"]
|
|
for parameter in requests[capability]["parameters"]
|
|
if parameter["name"] == "position_pct"
|
|
)
|
|
for capability in (
|
|
"seat.set_longitudinal_position",
|
|
"seat.set_cushion_tilt",
|
|
"seat.set_backrest_recline",
|
|
"seat.set_legrest_height",
|
|
)
|
|
)
|
|
|
|
self.assertEqual(actual_positions, expected_positions)
|
|
|
|
def test_common_modes_are_visible_with_vehicle_specific_overrides(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
common = root / "common"
|
|
denza = root / "vehicles" / "denza_n7"
|
|
other = root / "vehicles" / "other_vehicle"
|
|
common.mkdir(parents=True)
|
|
denza.mkdir(parents=True)
|
|
other.mkdir(parents=True)
|
|
(common / "relax.yaml").write_text(
|
|
"""
|
|
id: relax
|
|
name: 放松模式
|
|
aliases:
|
|
- relax
|
|
description: 公共放松模式。
|
|
operations:
|
|
- domain: roof
|
|
capability: roof.set_transmittance
|
|
target_scope: vehicle
|
|
operation: set
|
|
parameters:
|
|
transmittance_pct: 45
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(denza / "relax.yaml").write_text(
|
|
"""
|
|
id: relax
|
|
name: N7 放松模式
|
|
aliases:
|
|
- relax
|
|
description: N7 专属放松模式。
|
|
scope:
|
|
vehicle_ids:
|
|
- denza_n7
|
|
operations:
|
|
- domain: roof
|
|
capability: roof.set_transmittance
|
|
target_scope: vehicle
|
|
operation: set
|
|
parameters:
|
|
transmittance_pct: 20
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(other / "nap.yaml").write_text(
|
|
"""
|
|
id: nap
|
|
name: 午休模式
|
|
description: 其他车型专属午休模式。
|
|
operations:
|
|
- domain: roof
|
|
capability: roof.set_transmittance
|
|
target_scope: vehicle
|
|
operation: set
|
|
parameters:
|
|
transmittance_pct: 10
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
registry = ModeProfileRegistry(root)
|
|
|
|
denza_matches = registry.match_text("进入放松模式", vehicle_id="denza_n7")
|
|
other_matches = registry.match_text("进入放松模式", vehicle_id="other_vehicle")
|
|
|
|
self.assertEqual([profile.name for profile in denza_matches], ["N7 放松模式"])
|
|
self.assertEqual([profile.name for profile in other_matches], ["放松模式"])
|
|
self.assertEqual(
|
|
{profile.id for profile in registry.match_text("进入午休模式", vehicle_id="denza_n7")},
|
|
set(),
|
|
)
|
|
self.assertEqual(
|
|
{mode["id"] for mode in registry.public_modes(vehicle_id="denza_n7")},
|
|
{"relax"},
|
|
)
|
|
|
|
def test_save_mode_profile_persists_yaml_and_reloads(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
registry = ModeProfileRegistry(root)
|
|
profile = registry.save_mode(
|
|
"露营模式",
|
|
[
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_transmittance",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"transmittance_pct": 20},
|
|
"reason": "保存最近一次天幕目标。",
|
|
}
|
|
],
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
saved_path = Path(profile.source)
|
|
self.assertTrue(saved_path.is_file())
|
|
self.assertEqual(
|
|
saved_path.relative_to(root).parts[:3],
|
|
("vehicles", "denza_n7", "user"),
|
|
)
|
|
document = yaml.safe_load(saved_path.read_text(encoding="utf-8"))
|
|
self.assertEqual(document["name"], "露营模式")
|
|
self.assertEqual(document["scope"]["vehicle_ids"], ["denza_n7"])
|
|
self.assertEqual(document["operations"][0]["domain"], "roof")
|
|
|
|
reloaded = ModeProfileRegistry(root)
|
|
matched = reloaded.match_text("进入露营模式", vehicle_id="denza_n7")
|
|
self.assertEqual([item.name for item in matched], ["露营模式"])
|
|
self.assertEqual(
|
|
reloaded.match_text("进入露营模式", vehicle_id="other_vehicle"),
|
|
[],
|
|
)
|
|
requests = reloaded.requests_for_profiles(matched)
|
|
self.assertEqual(requests[0]["capability"], "roof.set_transmittance")
|
|
self.assertEqual(
|
|
requests[0]["parameters"],
|
|
[{"name": "transmittance_pct", "value": 20}],
|
|
)
|
|
|
|
def test_mode_card_override_persists_instruction_and_can_command_flow(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory) / "presets"
|
|
user_root = Path(directory) / "persistent-user-modes"
|
|
preset = root / "vehicles" / "denza_n7" / "office.yaml"
|
|
preset.parent.mkdir(parents=True)
|
|
preset.write_text(
|
|
"""
|
|
id: office
|
|
name: 办公模式
|
|
icon: briefcase
|
|
scope:
|
|
vehicle_ids: [denza_n7]
|
|
operations:
|
|
- domain: climate
|
|
capability: climate.set_temperature
|
|
target_scope: passenger
|
|
parameters:
|
|
temperature_c: 24
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
registry = ModeProfileRegistry(root, user_root=user_root)
|
|
flow = {
|
|
"schema": "cognitive-agent-mode-command-flow/v1",
|
|
"source_request_id": "req-mode-editor-001",
|
|
"plans": {
|
|
"native_climate": {
|
|
"spec": {
|
|
"commands": [
|
|
{
|
|
"message": "Native_0x1DE",
|
|
"signals": {
|
|
"frame_id": "0x1DE",
|
|
"data_hex": "00 00 00 00 18 00 00 00",
|
|
},
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
registry.save_mode(
|
|
"专注办公模式",
|
|
[
|
|
{
|
|
"domain": "climate",
|
|
"capability": "climate.set_temperature",
|
|
"target_scope": "passenger",
|
|
"parameters": {"temperature_c": 24},
|
|
}
|
|
],
|
|
mode_id="office",
|
|
vehicle_id="denza_n7",
|
|
icon="briefcase",
|
|
instruction="副驾空调设置为24度",
|
|
command_flow=flow,
|
|
)
|
|
|
|
reloaded = ModeProfileRegistry(root, user_root=user_root)
|
|
mode = next(
|
|
item
|
|
for item in reloaded.public_modes("denza_n7")
|
|
if item["id"] == "office"
|
|
)
|
|
self.assertEqual(mode["name"], "专注办公模式")
|
|
self.assertEqual(mode["origin"], "customized")
|
|
self.assertTrue(mode["preset_override"])
|
|
self.assertEqual(mode["instruction"], "副驾空调设置为24度")
|
|
self.assertEqual(
|
|
mode["command_flow"]["source_request_id"],
|
|
"req-mode-editor-001",
|
|
)
|
|
self.assertTrue(
|
|
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
|
|
)
|
|
|
|
def test_session_overlay_ignores_stale_repository_user_modes(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory) / "profiles"
|
|
session_root = Path(directory) / "session"
|
|
stale = root / "vehicles" / "denza_n7" / "user" / "stale.yaml"
|
|
stale.parent.mkdir(parents=True)
|
|
stale.write_text(
|
|
"""
|
|
id: stale
|
|
name: 上次运行模式
|
|
scope:
|
|
vehicle_ids: [denza_n7]
|
|
operations:
|
|
- domain: roof
|
|
capability: roof.set_transmittance
|
|
target_scope: vehicle
|
|
parameters:
|
|
transmittance_pct: 20
|
|
""".strip() + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
registry = ModeProfileRegistry(root, user_root=session_root)
|
|
|
|
self.assertEqual(registry.public_modes("denza_n7"), [])
|
|
created = registry.save_mode(
|
|
"本次运行模式",
|
|
[
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_transmittance",
|
|
"target_scope": "vehicle",
|
|
"parameters": {"transmittance_pct": 35},
|
|
}
|
|
],
|
|
vehicle_id="denza_n7",
|
|
)
|
|
self.assertTrue(Path(created.source).is_relative_to(session_root))
|
|
self.assertEqual(
|
|
{mode["id"] for mode in registry.public_modes("denza_n7")},
|
|
{created.id},
|
|
)
|
|
|
|
registry.delete_user_mode(created.id, "denza_n7")
|
|
self.assertFalse(Path(created.source).exists())
|
|
self.assertEqual(registry.public_modes("denza_n7"), [])
|
|
|
|
def test_delete_user_mode_removes_yaml_and_protects_presets(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
preset_path = root / "common" / "preset" / "office.yaml"
|
|
preset_path.parent.mkdir(parents=True)
|
|
preset_path.write_text(
|
|
"""
|
|
id: office
|
|
name: 办公模式
|
|
operations:
|
|
- domain: climate
|
|
capability: climate.set_temperature
|
|
target_scope: vehicle
|
|
parameters:
|
|
temperature_c: 24
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
registry = ModeProfileRegistry(root)
|
|
user_profile = registry.save_mode(
|
|
"K歌模式",
|
|
[
|
|
{
|
|
"domain": "ambient_light",
|
|
"capability": "ambient_light.set_power",
|
|
"target_scope": "vehicle",
|
|
"parameters": {"state": "on"},
|
|
}
|
|
],
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
public_modes = {
|
|
mode["id"]: mode
|
|
for mode in registry.public_modes(vehicle_id="denza_n7")
|
|
}
|
|
self.assertEqual(public_modes[user_profile.id]["origin"], "user")
|
|
self.assertTrue(public_modes[user_profile.id]["deletable"])
|
|
self.assertEqual(public_modes["office"]["origin"], "preset")
|
|
self.assertFalse(public_modes["office"]["deletable"])
|
|
|
|
deleted = registry.delete_user_mode(user_profile.id, "denza_n7")
|
|
|
|
self.assertEqual(deleted.id, user_profile.id)
|
|
self.assertFalse(Path(user_profile.source).exists())
|
|
self.assertNotIn(
|
|
user_profile.id,
|
|
{mode["id"] for mode in registry.public_modes("denza_n7")},
|
|
)
|
|
with self.assertRaises(PermissionError):
|
|
registry.delete_user_mode("office", "denza_n7")
|
|
|
|
def test_update_mode_profile_backs_up_initial_yaml_and_restores(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
registry = ModeProfileRegistry(root)
|
|
profile = registry.save_mode(
|
|
"露营模式",
|
|
[
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_transmittance",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"transmittance_pct": 20},
|
|
"reason": "原始天幕目标。",
|
|
}
|
|
],
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
updated = registry.update_mode(
|
|
profile,
|
|
[
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_transmittance",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"transmittance_pct": 50},
|
|
"reason": "用户修改天幕目标。",
|
|
}
|
|
],
|
|
)
|
|
|
|
self.assertTrue(Path(profile.source).with_suffix(".yaml.initial.bak").is_file())
|
|
requests = registry.requests_for_profiles([updated])
|
|
self.assertEqual(
|
|
requests[0]["parameters"],
|
|
[{"name": "transmittance_pct", "value": 50}],
|
|
)
|
|
|
|
restored = registry.restore_initial_mode(updated)
|
|
restored_requests = registry.requests_for_profiles([restored])
|
|
self.assertEqual(
|
|
restored_requests[0]["parameters"],
|
|
[{"name": "transmittance_pct", "value": 20}],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|