fix: restore mode clicks and compose seat-aware assistant scenes
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.scene_composer import normalize_scene_output
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def scene_service(tmp_path_factory):
|
||||
service = SeatDemoService()
|
||||
service.artifact_root = tmp_path_factory.mktemp("scene-explicit-seat-scope")
|
||||
service.mode_profiles = ModeProfileRegistry(
|
||||
REPOSITORY_ROOT / "config" / "modes",
|
||||
user_root=tmp_path_factory.mktemp("scene-explicit-seat-user-modes"),
|
||||
)
|
||||
try:
|
||||
yield service, AssistantCapabilityCatalog(service).scene_composition_context()
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
|
||||
def _plan(context, scopes):
|
||||
requests = []
|
||||
if "driver" in scopes:
|
||||
requests.append({
|
||||
"capability": "seat.set_driver_massage_native",
|
||||
"target_scope": "driver",
|
||||
"parameters": {"action": "mode", "mode": "relax"},
|
||||
})
|
||||
if "passenger" in scopes:
|
||||
requests.append({
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "passenger",
|
||||
"parameters": {"position_pct": 45},
|
||||
})
|
||||
requests.append({
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"temperature_c": 24},
|
||||
})
|
||||
return normalize_scene_output({
|
||||
"requests": requests, "ambiguities": [], "confidence": 0.95,
|
||||
}, context)
|
||||
|
||||
|
||||
def _preview(
|
||||
scene_service, message, scopes=("driver", "passenger"), *, execution_mode="hardware",
|
||||
):
|
||||
service, context = scene_service
|
||||
with patch.object(
|
||||
service.hardware_execution, "reserve",
|
||||
side_effect=AssertionError("scene scope validation must not execute hardware"),
|
||||
):
|
||||
return service.preview_native_can(
|
||||
message,
|
||||
provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope",
|
||||
execution_mode=execution_mode,
|
||||
vehicle_id="denza_n7",
|
||||
assistant_plan=_plan(context, scopes),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"构建一个主驾座椅舒适模式",
|
||||
"创建一个仅主驾舒适模式",
|
||||
"创建一个只给驾驶员坐着放松的功能模式",
|
||||
"构建一个副驾座椅舒适模式",
|
||||
"构建一个仅副驾舒适模式",
|
||||
"构建一个右前座椅舒适模式",
|
||||
"构建一个后排座椅舒适模式",
|
||||
"构建一个主驾座椅与副驾空调舒适模式",
|
||||
"构建一个主驾阅读灯与副驾座椅舒适模式",
|
||||
))
|
||||
def test_explicit_scene_seat_scope_rejects_cross_seat_plan_atomically(scene_service, message):
|
||||
result = _preview(scene_service, message)
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result.get("command_plan") is None
|
||||
assert result.get("command_plans") == {}
|
||||
intent = result.get("intent_ir") or {}
|
||||
assert (intent.get("spec") or {}).get("actions", []) == []
|
||||
assert any("座位范围" in item["message"] for item in result["validation"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("message", "scopes"), (
|
||||
("构建一个主驾座椅舒适模式", ("driver",)),
|
||||
("构建一个副驾座椅舒适模式", ("passenger",)),
|
||||
("创建一个仅主驾舒适模式", ("driver",)),
|
||||
("构建一个主驾和副驾座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个主副驾座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个云端座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个副驾空调与座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个座椅舒适与副驾阅读灯模式", ("driver", "passenger")),
|
||||
("构建一个主驾座椅与副驾空调舒适模式", ("driver",)),
|
||||
("构建一个主驾阅读灯与副驾座椅舒适模式", ("passenger",)),
|
||||
))
|
||||
def test_scene_scope_preserves_supported_targets_without_borrowing_other_device_zones(
|
||||
scene_service, message, scopes,
|
||||
):
|
||||
result = _preview(scene_service, message, scopes)
|
||||
assert result["status"] == "succeeded", result.get("validation")
|
||||
assert result["mode_profile_draft"]
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
actions = result["intent_ir"]["spec"]["actions"]
|
||||
assert {
|
||||
action["target"]["instance"]
|
||||
for action in actions if action["domain"] == "seat"
|
||||
} == set(scopes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"构建一个主驾舒适模式,副驾不要动",
|
||||
"构建一个主驾舒适模式,不要动副驾",
|
||||
"构建一个舒展模式,座椅靠背调整到45%",
|
||||
))
|
||||
def test_explicit_atomic_requirements_do_not_receive_scene_scope_relaxation(
|
||||
scene_service, message,
|
||||
):
|
||||
result = _preview(scene_service, message, execution_mode="dry_run")
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
|
||||
|
||||
def test_explicit_atomic_requirements_override_label_without_adding_model_actions(scene_service):
|
||||
result = _preview(scene_service, "构建一个主驾舒适模式,副驾靠背调整到45%")
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
actions = result["intent_ir"]["spec"]["actions"]
|
||||
assert len(actions) == 1
|
||||
assert actions[0]["capability"] == "seat.set_backrest_recline"
|
||||
assert actions[0]["target"]["instance"] == "passenger"
|
||||
assert actions[0]["parameters"]["position_pct"] == 45
|
||||
|
||||
|
||||
def test_rear_window_scene_is_not_rejected_as_an_unsupported_seat(scene_service):
|
||||
service, context = scene_service
|
||||
plan = normalize_scene_output({
|
||||
"requests": [{
|
||||
"capability": "window.set_position",
|
||||
"target_scope": "rear_right",
|
||||
"parameters": {"position_pct": 45},
|
||||
}],
|
||||
"ambiguities": [], "confidence": 0.95,
|
||||
}, context)
|
||||
result = service.preview_native_can(
|
||||
"构建一个后排换气模式", provider="local",
|
||||
conversation_id="conv-rear-window-not-seat",
|
||||
execution_mode="dry_run", vehicle_id="denza_n7", assistant_plan=plan,
|
||||
)
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["mode_profile_draft"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("text", "expected"), (
|
||||
("进入主驾舒适模式", frozenset({"driver"})),
|
||||
("进入仅副驾舒适模式", frozenset({"passenger"})),
|
||||
("进入主副驾座椅舒适模式", frozenset({"driver", "passenger"})),
|
||||
("进入主驾座椅与副驾的空调舒适模式", frozenset({"driver"})),
|
||||
("进入副驾座椅与主驾的阅读灯模式", frozenset({"passenger"})),
|
||||
("进入座椅舒适与右前车窗模式", None),
|
||||
("进入座椅舒适与后排阅读灯模式", None),
|
||||
("进入后排座椅舒适模式", frozenset({"unsupported"})),
|
||||
))
|
||||
def test_scene_scope_resolution_only_uses_seat_or_unqualified_scene_targets(text, expected):
|
||||
assert resolve_scene_seat_scopes(text) == expected
|
||||
|
||||
|
||||
def test_new_creation_cannot_borrow_draft_reference_to_bypass_explicit_scope(scene_service):
|
||||
service, context = scene_service
|
||||
built = _preview(scene_service, "构建一个主驾独享舒适模式", ("driver",))
|
||||
reference = {key: built["mode_profile_draft"][key] for key in (
|
||||
"name", "source_request_id", "vehicle_id",
|
||||
)}
|
||||
result = service.preview_native_can(
|
||||
"构建一个主驾独享舒适模式", provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
|
||||
vehicle_id="denza_n7", assistant_plan=_plan(context, ("driver", "passenger")),
|
||||
mode_profile_draft_context=reference,
|
||||
)
|
||||
assert result["status"] != "succeeded"
|
||||
assert not result.get("mode_profile_draft")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("initial_scope", "name", "change", "added_capability"), (
|
||||
("driver", "主驾释压舒适模式", "副驾座椅通风调整为1档", "seat.set_ventilation"),
|
||||
("passenger", "副驾释压舒适模式", "主驾座椅通风调整为低档", "seat.set_driver_ventilation_native"),
|
||||
))
|
||||
def test_explicit_revision_can_extend_old_label_scope_then_save_and_replay(
|
||||
scene_service, initial_scope, name, change, added_capability,
|
||||
):
|
||||
service, _context = scene_service
|
||||
built = _preview(scene_service, f"构建一个{name}", (initial_scope,))
|
||||
source_draft = built["mode_profile_draft"]
|
||||
reference = {key: source_draft[key] for key in (
|
||||
"name", "source_request_id", "vehicle_id",
|
||||
)}
|
||||
revised = service.preview_native_can(
|
||||
f"把{name}的{change}", provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
|
||||
vehicle_id="denza_n7", mode_profile_draft_context=reference,
|
||||
)
|
||||
assert revised["status"] == "succeeded", revised.get("validation")
|
||||
draft = revised["mode_profile_draft"]
|
||||
assert draft["generation_source"] == "draft_revision"
|
||||
assert draft["operation_count"] == source_draft["operation_count"] + 1
|
||||
actions = revised["intent_ir"]["spec"]["actions"]
|
||||
assert added_capability in {action["capability"] for action in actions}
|
||||
assert {
|
||||
action["target"]["instance"] for action in actions if action["domain"] == "seat"
|
||||
} == {"driver", "passenger"}
|
||||
saved = service.create_mode_profile_card(
|
||||
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
|
||||
)
|
||||
assert saved["status"] == "succeeded"
|
||||
replayed = service.execute_mode_profile_card(
|
||||
saved["mode_profile"]["id"], execution_mode="dry_run", vehicle_id="denza_n7",
|
||||
)
|
||||
assert replayed["status"] == "succeeded", replayed.get("validation")
|
||||
assert {
|
||||
(action["capability"], action["target"]["instance"])
|
||||
for action in replayed["intent_ir"]["spec"]["actions"] if action["domain"] == "seat"
|
||||
} == {
|
||||
(action["capability"], action["target"]["instance"])
|
||||
for action in actions if action["domain"] == "seat"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.codex_assistant import CodexAssistantService
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.scene_composer import normalize_scene_output
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
|
||||
|
||||
@@ -94,6 +95,7 @@ def _assistant_preview(
|
||||
client.available = False
|
||||
assistant = CodexAssistantService(client)
|
||||
snapshot = catalog.snapshot(vehicle_id="denza_n7")
|
||||
fast_answer = catalog.fast_answer(message, snapshot)
|
||||
events: list[dict[str, object]] = []
|
||||
raw_results: list[dict[str, object]] = []
|
||||
passed_plans: list[dict[str, object] | None] = []
|
||||
@@ -122,6 +124,7 @@ def _assistant_preview(
|
||||
conversation_id=conversation_id,
|
||||
principal="scene-template-hybrid-test",
|
||||
history=[],
|
||||
authoritative_answer=fast_answer.answer if fast_answer else None,
|
||||
emit=lambda event: events.append(event) or True,
|
||||
execute_control=execute_control,
|
||||
capability_snapshot=snapshot,
|
||||
@@ -141,7 +144,7 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
||||
templates = snapshot["scene_templates"]
|
||||
|
||||
assert isinstance(templates, list)
|
||||
assert len(templates) == 12
|
||||
assert len(templates) == 13
|
||||
assert all(template["assistant_callable"] is True for template in templates)
|
||||
|
||||
with patch.object(
|
||||
@@ -339,6 +342,115 @@ def test_explicit_settings_on_a_purpose_scene_bypass_template_defaults(
|
||||
client.start_turn.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected_name", "template_id"),
|
||||
(
|
||||
("构建一个座椅舒适的功能模式", "座椅舒适模式", "seat-comfort"),
|
||||
("构建一个让人坐着放松的功能模式", "坐着放松模式", "relaxation"),
|
||||
("请帮我创建一个适合乘坐舒适的模式吧", "乘坐舒适模式", "seat-comfort"),
|
||||
("帮我构建一个让我们坐着舒服的功能模式", "坐着舒服模式", "seat-comfort"),
|
||||
("创建一个座椅放松模式并预览", "座椅放松模式", "relaxation"),
|
||||
("构建一个长途舒适模式", "长途舒适模式", "long-trip-comfort"),
|
||||
),
|
||||
)
|
||||
def test_seating_comfort_requests_preview_both_seats_with_no_model_or_hardware_call(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
message: str,
|
||||
expected_name: str,
|
||||
template_id: str,
|
||||
) -> None:
|
||||
service, catalog, user_root = hybrid_service
|
||||
with patch.object(
|
||||
service.hardware_execution,
|
||||
"reserve",
|
||||
side_effect=AssertionError("seat comfort preview reached hardware execution"),
|
||||
):
|
||||
response, result, _events, client, _plans = _assistant_preview(
|
||||
service, catalog, message, conversation_id="conv-seat-comfort-preview",
|
||||
)
|
||||
assert response["route"] == "scene_template"
|
||||
assert response["success"] is True
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result["mode_profile_draft"]["name"] == expected_name
|
||||
assert result["mode_profile_draft"]["source_template_id"] == template_id
|
||||
seat_actions = {
|
||||
(capability, target): parameters
|
||||
for (capability, target), parameters in _action_map(result).items()
|
||||
if capability.startswith("seat.")
|
||||
}
|
||||
assert {target for _capability, target in seat_actions} == {"driver", "passenger"}
|
||||
assert all(
|
||||
"driver_axis" not in capability and "driver_position" not in capability
|
||||
for capability, _target in seat_actions
|
||||
)
|
||||
assert ("seat.set_driver_massage_native", "passenger") not in seat_actions
|
||||
if template_id in {"seat-comfort", "long-trip-comfort"}:
|
||||
assert seat_actions["seat.set_driver_ventilation_native", "driver"] == {
|
||||
"state": "on", "level": 2,
|
||||
}
|
||||
assert seat_actions["seat.set_ventilation", "passenger"] == {
|
||||
"state": "on", "level": 1,
|
||||
}
|
||||
client.start_structured_turn.assert_not_called()
|
||||
client.start_turn.assert_not_called()
|
||||
assert not list(user_root.rglob("*.yaml"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ("云端座椅舒展模式", "晚间乘坐释压模式"))
|
||||
def test_unregistered_seat_scene_keeps_both_explicit_targets_through_native_merge(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
name: str,
|
||||
) -> None:
|
||||
service, catalog, user_root = hybrid_service
|
||||
plan = normalize_scene_output({
|
||||
"requests": [
|
||||
{"capability": "seat.set_driver_massage_native", "target_scope": "driver",
|
||||
"parameters": {"action": "mode", "mode": "relax"}},
|
||||
{"capability": "seat.set_driver_heating_native", "target_scope": "driver",
|
||||
"parameters": {"state": "on", "level": 2}},
|
||||
{"capability": "seat.set_backrest_recline", "target_scope": "passenger",
|
||||
"parameters": {"position_pct": 45}},
|
||||
{"capability": "seat.set_heating", "target_scope": "passenger",
|
||||
"parameters": {"state": "on", "level": 1}},
|
||||
],
|
||||
"ambiguities": [], "confidence": 0.95,
|
||||
}, catalog.scene_composition_context())
|
||||
with patch.object(
|
||||
service.hardware_execution, "reserve",
|
||||
side_effect=AssertionError("scene composition reached hardware execution"),
|
||||
):
|
||||
result = service.preview_native_can(
|
||||
f"构建一个{name}", provider="local", conversation_id="conv-seat-generated",
|
||||
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
|
||||
)
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["mode_profile_draft"]["generation_source"] == "codex_scene_composition"
|
||||
assert set(_action_map(result)) == {
|
||||
("seat.set_driver_massage_native", "driver"),
|
||||
("seat.set_driver_heating_native", "driver"),
|
||||
("seat.set_backrest_recline", "passenger"),
|
||||
("seat.set_heating", "passenger"),
|
||||
}
|
||||
assert not list(user_root.rglob("*.yaml"))
|
||||
|
||||
|
||||
def test_explicit_unscoped_seat_instruction_keeps_target_ambiguity_guard(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
service, _catalog, _user_root = hybrid_service
|
||||
result = service.preview_native_can(
|
||||
"构建一个舒展模式,座椅靠背调整到45%", provider="local",
|
||||
conversation_id="conv-seat-explicit-target", execution_mode="hardware",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
|
||||
|
||||
def test_template_metadata_cannot_authorize_modified_or_stale_template_actions(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user