feat: add seat-scoped static and dynamic assistant modes
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
"""Seat-aware built-in defaults must remain safe and preserve saved user intent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
|
||||
COMFORT_TEMPLATE_IDS = (
|
||||
"reading",
|
||||
"cinema",
|
||||
"camping",
|
||||
"quiet-night",
|
||||
"reception",
|
||||
"romantic",
|
||||
"winter-warmth",
|
||||
"summer-cooling",
|
||||
"long-trip-comfort",
|
||||
"relaxation",
|
||||
"seat-comfort",
|
||||
)
|
||||
DRIVER_COMFORT_CAPABILITIES = {
|
||||
"seat.set_driver_ventilation_native",
|
||||
"seat.set_driver_heating_native",
|
||||
"seat.set_driver_massage_native",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(tmp_path: Path) -> Iterator[SeatDemoService]:
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
|
||||
},
|
||||
):
|
||||
instance = SeatDemoService(REPOSITORY_ROOT)
|
||||
instance.artifact_root = tmp_path / "runs"
|
||||
instance.mode_profiles = ModeProfileRegistry(
|
||||
REPOSITORY_ROOT / "config" / "modes",
|
||||
user_root=tmp_path / "user-modes",
|
||||
)
|
||||
try:
|
||||
with patch.object(
|
||||
instance.hardware_execution,
|
||||
"reserve",
|
||||
side_effect=AssertionError("creating or previewing a mode reached real hardware"),
|
||||
):
|
||||
yield instance
|
||||
finally:
|
||||
instance.close()
|
||||
|
||||
|
||||
def _preview(service: SeatDemoService, instruction: str, *, plan=None) -> dict:
|
||||
result = service.preview_native_can(
|
||||
instruction,
|
||||
provider="local",
|
||||
conversation_id="conv-registered-seat-coverage",
|
||||
execution_mode="hardware",
|
||||
vehicle_id="denza_n7",
|
||||
assistant_plan=plan,
|
||||
)
|
||||
assert result["status"] == "succeeded", result.get("validation")
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
return result
|
||||
|
||||
|
||||
def _action_map(result: dict) -> dict:
|
||||
return {
|
||||
(
|
||||
action["capability"],
|
||||
action["target"].get("instance") or action["target"].get("zone"),
|
||||
): action["parameters"]
|
||||
for action in result["intent_ir"]["spec"]["actions"]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template_id", COMFORT_TEMPLATE_IDS)
|
||||
@pytest.mark.parametrize(("seat_prefix", "targets"), (
|
||||
("", {"passenger"}), ("主驾", {"driver"}),
|
||||
("副驾", {"passenger"}), ("主驾和副驾", {"driver", "passenger"}),
|
||||
))
|
||||
def test_comfort_templates_select_only_requested_seat_without_mutating_seat_seeds(
|
||||
service: SeatDemoService,
|
||||
template_id: str,
|
||||
seat_prefix: str,
|
||||
targets: set[str],
|
||||
) -> None:
|
||||
template = service.scene_templates.get_template(template_id)
|
||||
original_revision = template.revision
|
||||
result = _preview(service, f"构建一个{seat_prefix}{template.name}", plan=template.plan())
|
||||
actions = _action_map(result)
|
||||
seat_keys = {key for key in actions if key[0].startswith("seat.")}
|
||||
assert {target for _, target in seat_keys} == targets
|
||||
assert {
|
||||
operation.target_scope for operation in template.operations if operation.domain == "seat"
|
||||
} == {"driver", "passenger"}
|
||||
assert all(
|
||||
operation.capability in DRIVER_COMFORT_CAPABILITIES
|
||||
for operation in template.operations
|
||||
if operation.domain == "seat" and operation.target_scope == "driver"
|
||||
), "driver positional or timed movement must not enter static comfort defaults"
|
||||
assert not any(
|
||||
"massage" in capability for capability, target in seat_keys if target == "passenger"
|
||||
)
|
||||
assert actions == {
|
||||
(operation.capability, operation.target_scope): operation.parameters
|
||||
for operation in template.operations
|
||||
if operation.domain != "seat" or operation.target_scope in targets
|
||||
}
|
||||
assert result["mode_profile_draft"]["generation_source"] == "scene_template_copy"
|
||||
assert service.scene_templates.get_template(template_id).revision == original_revision
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode_id", "mode_name"),
|
||||
(
|
||||
("office", "办公模式"),
|
||||
("nap", "小憩模式"),
|
||||
("meditation_space", "冥想空间模式"),
|
||||
("leisure_lounger", "休闲躺椅模式"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(("seat_prefix", "targets"), (
|
||||
("", {"passenger"}), ("主驾", {"driver"}),
|
||||
("副驾", {"passenger"}), ("主驾和副驾", {"driver", "passenger"}),
|
||||
))
|
||||
def test_registered_cabin_modes_select_only_requested_seat_on_creation(
|
||||
service: SeatDemoService,
|
||||
mode_id: str,
|
||||
mode_name: str,
|
||||
seat_prefix: str,
|
||||
targets: set[str],
|
||||
) -> None:
|
||||
result = _preview(service, f"构建一个{seat_prefix}{mode_name}")
|
||||
actions = _action_map(result)
|
||||
assert {target for capability, target in actions if capability.startswith("seat.")} == targets
|
||||
if "passenger" in targets:
|
||||
assert ("seat.set_backrest_recline", "passenger") in actions
|
||||
draft = result["mode_profile_draft"]
|
||||
if seat_prefix:
|
||||
assert "existing_mode_id" not in draft, "a selected-seat copy must not overwrite the original preset"
|
||||
else:
|
||||
assert draft["existing_mode_id"] == mode_id
|
||||
assert draft["generation_source"] == "registered_mode_profile"
|
||||
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
|
||||
assert actions == {
|
||||
(operation.capability, operation.target_scope): operation.parameters
|
||||
for operation in profile.operations
|
||||
if operation.domain != "seat" or operation.target_scope in targets
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template_id", ("fresh-air", "rain-defrost"))
|
||||
def test_air_quality_and_visibility_templates_do_not_invent_seat_actions(
|
||||
service: SeatDemoService,
|
||||
template_id: str,
|
||||
) -> None:
|
||||
template = service.scene_templates.get_template(template_id)
|
||||
result = _preview(service, f"构建一个{template.name}", plan=template.plan())
|
||||
assert not any(capability.startswith("seat.") for capability, _ in _action_map(result))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"template_id",
|
||||
(
|
||||
"long-trip-comfort",
|
||||
"summer-cooling",
|
||||
"winter-warmth",
|
||||
"reception",
|
||||
"quiet-night",
|
||||
"romantic",
|
||||
),
|
||||
)
|
||||
def test_non_posture_templates_keep_both_seat_positions_unchanged(
|
||||
service: SeatDemoService,
|
||||
template_id: str,
|
||||
) -> None:
|
||||
template = service.scene_templates.get_template(template_id)
|
||||
assert all(
|
||||
"position_pct" not in operation.parameters and "duration_ms" not in operation.parameters
|
||||
for operation in template.operations
|
||||
if operation.domain == "seat"
|
||||
)
|
||||
|
||||
|
||||
def test_native_driver_and_passenger_thermal_levels_are_not_confused(
|
||||
service: SeatDemoService,
|
||||
) -> None:
|
||||
for template_id in (
|
||||
"reading",
|
||||
"camping",
|
||||
"quiet-night",
|
||||
"reception",
|
||||
"romantic",
|
||||
"long-trip-comfort",
|
||||
"seat-comfort",
|
||||
):
|
||||
template = service.scene_templates.get_template(template_id)
|
||||
params = {(op.capability, op.target_scope): op.parameters for op in template.operations}
|
||||
assert params[("seat.set_driver_ventilation_native", "driver")] == {
|
||||
"state": "on",
|
||||
"level": 2,
|
||||
}
|
||||
assert params[("seat.set_ventilation", "passenger")] == {"state": "on", "level": 1}
|
||||
|
||||
|
||||
def test_upgraded_template_does_not_silently_add_seats_to_a_saved_custom_mode(
|
||||
service: SeatDemoService,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
# Simulate a previously saved user preference with the same name as a template.
|
||||
original = _preview(service, "构建一个观影模式,空调温度调整到25度")
|
||||
draft = original["mode_profile_draft"]
|
||||
assert len(_action_map(original)) == 1
|
||||
saved = service.create_mode_profile_card(
|
||||
draft["name"],
|
||||
draft["instruction"],
|
||||
draft["source_request_id"],
|
||||
draft["vehicle_id"],
|
||||
)
|
||||
assert saved["status"] == "succeeded"
|
||||
paths = list((tmp_path / "user-modes").rglob("*.yaml"))
|
||||
hashes = {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths}
|
||||
|
||||
repeated = _preview(service, "构建一个观影模式")
|
||||
assert repeated["mode_profile_draft"]["generation_source"] == "registered_mode_profile"
|
||||
assert _action_map(repeated) == _action_map(original)
|
||||
assert {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths} == hashes
|
||||
assert len(service.scene_templates.get_template("cinema").operations) == 11
|
||||
|
||||
|
||||
def test_explicit_seat_only_requirement_is_not_expanded_to_other_seat_defaults(
|
||||
service: SeatDemoService,
|
||||
) -> None:
|
||||
result = _preview(service, "构建一个阅读模式,副驾座椅通风关闭")
|
||||
assert _action_map(result) == {("seat.set_ventilation", "passenger"): {"state": "off"}}
|
||||
assert result["mode_profile_draft"]["generation_source"] == "explicit_scene_requirements"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode_id", ("office", "nap", "meditation_space", "leisure_lounger"))
|
||||
@pytest.mark.parametrize(("seat", "spoken_seat"), (("driver", "主驾"), ("passenger", "副驾驶")))
|
||||
def test_explicit_single_seat_invocation_selects_without_mutating_cabin_mode(
|
||||
service: SeatDemoService,
|
||||
mode_id: str,
|
||||
seat: str,
|
||||
spoken_seat: str,
|
||||
) -> None:
|
||||
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
|
||||
original_revision = service.mode_profiles.profile_revision(profile)
|
||||
expected = {
|
||||
(operation.capability, operation.target_scope): operation.parameters
|
||||
for operation in profile.operations
|
||||
if operation.domain != "seat" or operation.target_scope == seat
|
||||
}
|
||||
result = service.preview_native_can(
|
||||
f"请让{spoken_seat}进入{profile.name}",
|
||||
provider="local",
|
||||
conversation_id="conv-explicit-seat-mode-selection",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
assert result["status"] == "succeeded", result.get("validation")
|
||||
assert _action_map(result) == expected
|
||||
assert (
|
||||
service.mode_profiles.profile_revision(
|
||||
service.mode_profiles.get_profile(mode_id, "denza_n7"),
|
||||
)
|
||||
== original_revision
|
||||
)
|
||||
assert any(
|
||||
operation.target_scope != seat
|
||||
for operation in service.mode_profiles.get_profile(mode_id, "denza_n7").operations
|
||||
if operation.domain == "seat"
|
||||
), "single-seat execution must not edit the persisted dual-seat profile"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode_id", "spoken_seat"),
|
||||
[
|
||||
(mode_id, seat)
|
||||
for mode_id in ("zero_gravity", "easy_entry_exit", "rocking_horse", "office")
|
||||
for seat in ("主驾", "后排", "所有座椅")
|
||||
if (mode_id, seat) != ("office", "主驾")
|
||||
],
|
||||
)
|
||||
def test_mode_invocation_still_rejects_genuinely_unsupported_seat_scopes(
|
||||
service: SeatDemoService,
|
||||
mode_id: str,
|
||||
spoken_seat: str,
|
||||
) -> None:
|
||||
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
|
||||
result = service.preview_native_can(
|
||||
f"请让{spoken_seat}进入{profile.name}",
|
||||
provider="local",
|
||||
conversation_id="conv-explicit-seat-mode-rejection",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
assert result["status"] in {"no_action", "needs_confirmation"}
|
||||
assert _action_map(result) == {}
|
||||
assert any(
|
||||
issue.get("reason") == "mode_target_scope_mismatch"
|
||||
for issue in result["intent_ir"]["spec"].get("ambiguities", [])
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"message",
|
||||
("创建一个主驾办公模式", "修改办公模式,只要主驾", "主驾办公模式"),
|
||||
)
|
||||
def test_mode_seat_selection_requires_an_explicit_invocation_not_configuration(
|
||||
service: SeatDemoService,
|
||||
message: str,
|
||||
) -> None:
|
||||
profile = service.mode_profiles.get_profile("office", "denza_n7")
|
||||
assert service._explicit_mode_seat_selection(profile, message) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode_id", ("office", "nap", "meditation_space", "leisure_lounger"))
|
||||
def test_unscoped_mode_invocation_keeps_registered_native_driver_actions(
|
||||
service: SeatDemoService,
|
||||
mode_id: str,
|
||||
) -> None:
|
||||
profile = service.mode_profiles.get_profile(mode_id, "denza_n7")
|
||||
result = service.preview_native_can(
|
||||
f"进入{profile.name}",
|
||||
provider="local",
|
||||
conversation_id="conv-mode-native-seat-preserved",
|
||||
execution_mode="dry_run",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
assert result["status"] == "succeeded"
|
||||
assert _action_map(result) == {
|
||||
(operation.capability, operation.target_scope): operation.parameters
|
||||
for operation in profile.operations
|
||||
}
|
||||
Reference in New Issue
Block a user