563 lines
18 KiB
Python
563 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
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 ModeProfile, ModeProfileRegistry
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
|
|
|
READING_OPERATIONS: list[dict[str, object]] = [
|
|
{
|
|
"domain": "climate",
|
|
"capability": "climate.set_power",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"state": "on"},
|
|
"reason": "阅读时保持空调开启。",
|
|
},
|
|
{
|
|
"domain": "climate",
|
|
"capability": "climate.set_temperature",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"temperature_c": 24},
|
|
"reason": "阅读模式默认温度。",
|
|
},
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_shade_position",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": {"position_pct": 30},
|
|
"reason": "减少阅读眩光。",
|
|
},
|
|
{
|
|
"domain": "interior_light",
|
|
"capability": "interior_light.set_reading_power",
|
|
"target_scope": "all",
|
|
"operation": "set",
|
|
"parameters": {"state": "on"},
|
|
"reason": "打开阅读灯。",
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def vehicle_service(tmp_path: Path) -> SeatDemoService:
|
|
service = SeatDemoService(REPOSITORY_ROOT)
|
|
service.artifact_root = tmp_path / "runs"
|
|
service.mode_profiles = ModeProfileRegistry(
|
|
REPOSITORY_ROOT / "config" / "modes",
|
|
user_root=tmp_path / "user-modes",
|
|
)
|
|
return service
|
|
|
|
|
|
def _save_reading_mode(service: SeatDemoService) -> ModeProfile:
|
|
return service.mode_profiles.save_mode(
|
|
"阅读模式",
|
|
READING_OPERATIONS,
|
|
mode_id="user-reading",
|
|
aliases=["看书模式", "阅读空间模式"],
|
|
vehicle_id="denza_n7",
|
|
description="用户保存的阅读场景。",
|
|
instruction="空调24度,遮阳帘30%,打开阅读灯",
|
|
)
|
|
|
|
|
|
def _save_office_override(service: SeatDemoService) -> ModeProfile:
|
|
preset = service.mode_profiles.get_profile("office", "denza_n7")
|
|
return service.mode_profiles.save_mode(
|
|
preset.name,
|
|
list(preset.operations),
|
|
mode_id=preset.id,
|
|
aliases=list(preset.aliases),
|
|
vehicle_id="denza_n7",
|
|
description=preset.description,
|
|
icon=preset.icon,
|
|
instruction=preset.instruction,
|
|
command_flow=preset.command_flow,
|
|
timeline=preset.timeline,
|
|
)
|
|
|
|
|
|
def _temperature(profile: ModeProfile, scope: str) -> object:
|
|
return next(
|
|
operation.parameters["temperature_c"]
|
|
for operation in profile.operations
|
|
if operation.capability == "climate.set_temperature"
|
|
and operation.target_scope == scope
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("message", "profile_factory", "mode_id", "target_scope", "temperature"),
|
|
(
|
|
(
|
|
"把阅读模式中空调温度调整到26度",
|
|
_save_reading_mode,
|
|
"user-reading",
|
|
"vehicle",
|
|
26,
|
|
),
|
|
(
|
|
"调整办公模式,把办公模式的副驾空调温度调整为26度。",
|
|
_save_office_override,
|
|
"office",
|
|
"passenger",
|
|
26,
|
|
),
|
|
(
|
|
"将看书模式里的空调温度改成25度",
|
|
_save_reading_mode,
|
|
"user-reading",
|
|
"vehicle",
|
|
25,
|
|
),
|
|
(
|
|
"请帮我把“阅读模式”里的空调温度改成25度吧。",
|
|
_save_reading_mode,
|
|
"user-reading",
|
|
"vehicle",
|
|
25,
|
|
),
|
|
),
|
|
)
|
|
def test_saved_mode_natural_language_update_is_control_plane_only(
|
|
vehicle_service: SeatDemoService,
|
|
message: str,
|
|
profile_factory: object,
|
|
mode_id: str,
|
|
target_scope: str,
|
|
temperature: int,
|
|
) -> None:
|
|
assert callable(profile_factory)
|
|
profile_factory(vehicle_service)
|
|
|
|
with patch.object(
|
|
vehicle_service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("saved-mode update acquired the hardware lease"),
|
|
):
|
|
result = vehicle_service.preview_native_can(
|
|
message,
|
|
provider="local",
|
|
conversation_id=f"conv-saved-mode-update-{temperature}-{mode_id}",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
updated = vehicle_service.mode_profiles.get_profile(mode_id, "denza_n7")
|
|
assert result["status"] == "succeeded"
|
|
assert result["orchestration_route"] == "mode_profile_update"
|
|
assert result["execution_mode"] == "dry_run"
|
|
assert result["hardware_execution_allowed"] is False
|
|
assert result["mode_profile"]["id"] == mode_id
|
|
assert result["command_plans"] == {}
|
|
assert result["can_commands"] == []
|
|
assert _temperature(updated, target_scope) == temperature
|
|
assert sum(
|
|
operation.capability == "climate.set_temperature"
|
|
and operation.target_scope == target_scope
|
|
for operation in updated.operations
|
|
) == 1
|
|
|
|
|
|
def test_builtin_mode_update_is_copy_on_write_and_delete_reveals_preset(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
preset = vehicle_service.mode_profiles.get_profile("office", "denza_n7")
|
|
preset_path = Path(preset.source)
|
|
original_bytes = preset_path.read_bytes()
|
|
original_temperature = _temperature(preset, "passenger")
|
|
|
|
updated = vehicle_service.preview_native_can(
|
|
"把办公模式中的副驾空调温度调整到26度",
|
|
provider="local",
|
|
conversation_id="conv-office-copy-on-write",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
effective = vehicle_service.mode_profiles.get_profile("office", "denza_n7")
|
|
public = next(
|
|
item
|
|
for item in vehicle_service.mode_profiles.public_modes("denza_n7")
|
|
if item["id"] == "office"
|
|
)
|
|
assert updated["status"] == "succeeded"
|
|
assert updated["mode_profile"]["origin"] == "customized"
|
|
assert effective.user_defined is True
|
|
assert _temperature(effective, "passenger") == 26
|
|
assert preset_path.read_bytes() == original_bytes
|
|
assert public["origin"] == "customized"
|
|
|
|
deleted = vehicle_service.preview_native_can(
|
|
"删除办公模式",
|
|
provider="local",
|
|
conversation_id="conv-office-copy-on-write-delete",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
restored = vehicle_service.mode_profiles.get_profile("office", "denza_n7")
|
|
assert deleted["status"] == "succeeded"
|
|
assert restored.user_defined is False
|
|
assert _temperature(restored, "passenger") == original_temperature
|
|
assert preset_path.read_bytes() == original_bytes
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("message", "capability", "target", "parameter", "expected"),
|
|
(
|
|
(
|
|
"把阅读模式中的阅读灯关闭",
|
|
"interior_light.set_reading_power",
|
|
"all",
|
|
"state",
|
|
"off",
|
|
),
|
|
(
|
|
"把阅读模式中的遮阳帘调整到80%",
|
|
"roof.set_shade_position",
|
|
"vehicle",
|
|
"position_pct",
|
|
80,
|
|
),
|
|
(
|
|
"把阅读模式中的左前车窗调整到20%",
|
|
"window.set_position",
|
|
"front_left",
|
|
"position_pct",
|
|
20,
|
|
),
|
|
(
|
|
"把阅读模式中的主驾座椅通风调整到2档",
|
|
"seat.set_driver_ventilation_native",
|
|
"driver",
|
|
"level",
|
|
2,
|
|
),
|
|
),
|
|
)
|
|
def test_saved_mode_update_persists_native_can_capability_families(
|
|
vehicle_service: SeatDemoService,
|
|
message: str,
|
|
capability: str,
|
|
target: str,
|
|
parameter: str,
|
|
expected: object,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
|
|
result = vehicle_service.preview_native_can(
|
|
message,
|
|
provider="local",
|
|
conversation_id=f"conv-native-mode-update-{parameter.replace('_', '-')}",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
updated = vehicle_service.mode_profiles.get_profile(
|
|
"user-reading",
|
|
"denza_n7",
|
|
)
|
|
operation = next(
|
|
item
|
|
for item in updated.operations
|
|
if item.capability == capability and item.target_scope == target
|
|
)
|
|
assert result["status"] == "succeeded"
|
|
assert result["deferred_requests"] == []
|
|
assert operation.parameters[parameter] == expected
|
|
|
|
|
|
def test_failed_saved_mode_update_is_atomic_and_reports_the_missing_mode(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
original = vehicle_service.mode_profiles.get_profile(
|
|
"user-reading", "denza_n7"
|
|
)
|
|
original_revision = vehicle_service.mode_profiles.profile_revision(original)
|
|
|
|
missing = vehicle_service.preview_native_can(
|
|
"把不存在模式中的空调温度调整到26度",
|
|
provider="local",
|
|
conversation_id="conv-saved-mode-update-missing",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
invalid = vehicle_service.preview_native_can(
|
|
"把阅读模式中的曲速引擎调整到100%",
|
|
provider="local",
|
|
conversation_id="conv-saved-mode-update-invalid",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
unchanged = vehicle_service.mode_profiles.get_profile(
|
|
"user-reading", "denza_n7"
|
|
)
|
|
assert missing["status"] == "needs_confirmation"
|
|
assert missing["orchestration_route"] == "mode_profile_update"
|
|
assert any(
|
|
item.get("code") == "MODE_PROFILE_NOT_FOUND"
|
|
for item in missing["validation"]
|
|
)
|
|
assert invalid["status"] != "succeeded"
|
|
assert invalid["orchestration_route"] == "mode_profile_update"
|
|
assert invalid["mode_profile"] is None
|
|
assert vehicle_service.mode_profiles.profile_revision(unchanged) == original_revision
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"删除阅读模式",
|
|
"请帮我删除一下看书模式吧",
|
|
"把“阅读模式”删掉。",
|
|
),
|
|
)
|
|
def test_saved_user_mode_can_be_deleted_by_name_or_alias_without_hardware(
|
|
vehicle_service: SeatDemoService,
|
|
message: str,
|
|
) -> None:
|
|
saved = _save_reading_mode(vehicle_service)
|
|
|
|
with patch.object(
|
|
vehicle_service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("saved-mode deletion acquired the hardware lease"),
|
|
):
|
|
result = vehicle_service.preview_native_can(
|
|
message,
|
|
provider="local",
|
|
conversation_id="conv-saved-mode-delete-user",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
assert result["status"] == "succeeded"
|
|
assert result["orchestration_route"] == "mode_profile_delete"
|
|
assert result["execution_mode"] == "dry_run"
|
|
assert result["hardware_execution_allowed"] is False
|
|
assert result["deleted_mode_profile"]["id"] == saved.id
|
|
assert result["deleted_mode_profile"]["name"] == "阅读模式"
|
|
assert result["command_plans"] == {}
|
|
assert result["can_commands"] == []
|
|
assert vehicle_service.mode_profiles.resolve_exact_name(
|
|
"阅读模式", "denza_n7"
|
|
) is None
|
|
|
|
|
|
def test_delete_missing_and_builtin_modes_fails_closed_without_changing_registry(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
preset = vehicle_service.mode_profiles.get_profile("office", "denza_n7")
|
|
preset_revision = vehicle_service.mode_profiles.profile_revision(preset)
|
|
|
|
missing = vehicle_service.preview_native_can(
|
|
"删除不存在模式",
|
|
provider="local",
|
|
conversation_id="conv-saved-mode-delete-missing",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
protected = vehicle_service.preview_native_can(
|
|
"删除办公模式",
|
|
provider="local",
|
|
conversation_id="conv-saved-mode-delete-protected",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
assert missing["status"] == "needs_confirmation"
|
|
assert missing["orchestration_route"] == "mode_profile_delete"
|
|
assert any(
|
|
item.get("code") == "MODE_PROFILE_NOT_FOUND"
|
|
for item in missing["validation"]
|
|
)
|
|
assert protected["status"] == "rejected"
|
|
assert protected["orchestration_route"] == "mode_profile_delete"
|
|
assert any(
|
|
item.get("code") == "MODE_PROFILE_DELETE_PROTECTED"
|
|
for item in protected["validation"]
|
|
)
|
|
remaining = vehicle_service.mode_profiles.get_profile("office", "denza_n7")
|
|
assert vehicle_service.mode_profiles.profile_revision(remaining) == preset_revision
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"阅读模式中会控制哪些设备,执行哪些动作?",
|
|
"看书模式里有哪些设备和参数",
|
|
"阅读空间模式会做什么?",
|
|
),
|
|
)
|
|
def test_saved_mode_details_are_answered_from_the_live_registry_without_model(
|
|
vehicle_service: SeatDemoService,
|
|
message: str,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
catalog = AssistantCapabilityCatalog(vehicle_service)
|
|
snapshot = catalog.snapshot("denza_n7")
|
|
|
|
answer = catalog.fast_answer(message, snapshot)
|
|
|
|
assert answer is not None
|
|
assert answer.topic == "specific_mode_detail"
|
|
assert "阅读模式" in answer.answer
|
|
assert "空调" in answer.answer
|
|
assert "24" in answer.answer
|
|
assert "遮阳帘" in answer.answer
|
|
assert "阅读灯" in answer.answer
|
|
assert "未下发车辆控制" in answer.answer
|
|
|
|
|
|
def test_saved_mode_detail_query_reports_an_unknown_exact_name_without_guessing(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
catalog = AssistantCapabilityCatalog(vehicle_service)
|
|
snapshot = catalog.snapshot("denza_n7")
|
|
|
|
answer = catalog.fast_answer("火星模式包含哪些设备和动作?", snapshot)
|
|
|
|
assert answer is not None
|
|
assert answer.topic == "specific_mode_detail_not_found"
|
|
assert "火星模式" in answer.answer
|
|
assert "未找到" in answer.answer
|
|
assert "未下发车辆控制" in answer.answer
|
|
assert "阅读模式" not in answer.answer
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"把阅读模式中空调温度调整到26度",
|
|
"删除阅读模式",
|
|
"执行阅读模式",
|
|
"把全车空调温度调整到26度",
|
|
),
|
|
)
|
|
def test_saved_mode_catalog_answer_never_intercepts_mutation_or_execution(
|
|
vehicle_service: SeatDemoService,
|
|
message: str,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
catalog = AssistantCapabilityCatalog(vehicle_service)
|
|
snapshot = catalog.snapshot("denza_n7")
|
|
|
|
assert catalog.fast_answer(message, snapshot) is None
|
|
|
|
|
|
def _assistant_turn(
|
|
service: SeatDemoService,
|
|
message: str,
|
|
conversation_id: str,
|
|
) -> tuple[dict[str, object], list[dict[str, object]], MagicMock]:
|
|
client = MagicMock()
|
|
client.available = False
|
|
client.status = "unavailable"
|
|
assistant = CodexAssistantService(client)
|
|
catalog = AssistantCapabilityCatalog(service)
|
|
snapshot = catalog.snapshot("denza_n7")
|
|
events: list[dict[str, object]] = []
|
|
|
|
def execute_control(
|
|
instruction: str,
|
|
progress: object,
|
|
_assistant_plan: object = None,
|
|
) -> dict[str, object]:
|
|
return service.preview_native_can(
|
|
instruction,
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
on_commands_validated=progress if callable(progress) else None,
|
|
)
|
|
|
|
response = assistant.stream(
|
|
message,
|
|
conversation_id=conversation_id,
|
|
principal="saved-mode-management-test",
|
|
history=[],
|
|
emit=lambda event: events.append(event) or True,
|
|
execute_control=execute_control,
|
|
capability_snapshot=snapshot,
|
|
capability_context=catalog.compact_context(snapshot),
|
|
scene_capability_context=catalog.scene_composition_context(snapshot),
|
|
control_terms=catalog.control_terms(snapshot),
|
|
query_capabilities=catalog.query,
|
|
)
|
|
return response, events, client
|
|
|
|
|
|
def test_assistant_updates_saved_mode_without_waiting_for_codex_tool_call(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
|
|
with patch.object(
|
|
vehicle_service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("assistant update acquired the hardware lease"),
|
|
):
|
|
response, events, client = _assistant_turn(
|
|
vehicle_service,
|
|
"把阅读模式中空调温度调整到26度",
|
|
"conv-assistant-saved-mode-update",
|
|
)
|
|
|
|
assert response["route"] == "mode_profile_update"
|
|
assert response["success"] is True
|
|
assert "阅读模式" in response["answer"]
|
|
assert "26" in response["answer"]
|
|
assert "车辆控制未完成" not in response["answer"]
|
|
assert events[0]["route"] == "mode_profile_update"
|
|
assert events[0]["engine"] == "n7web-mode-profile-manager"
|
|
assert events[-1]["route"] == "mode_profile_update"
|
|
assert next(event for event in events if event["type"] == "action.completed")[
|
|
"success"
|
|
] is True
|
|
assert _temperature(
|
|
vehicle_service.mode_profiles.get_profile("user-reading", "denza_n7"),
|
|
"vehicle",
|
|
) == 26
|
|
client.start_thread.assert_not_called()
|
|
client.start_turn.assert_not_called()
|
|
|
|
|
|
def test_assistant_deletes_saved_user_mode_without_waiting_for_codex_tool_call(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
_save_reading_mode(vehicle_service)
|
|
|
|
response, events, client = _assistant_turn(
|
|
vehicle_service,
|
|
"删除阅读模式",
|
|
"conv-assistant-saved-mode-delete",
|
|
)
|
|
|
|
assert response["route"] == "mode_profile_delete"
|
|
assert response["success"] is True
|
|
assert "阅读模式" in response["answer"]
|
|
assert "删除" in response["answer"]
|
|
assert "仍保留" not in response["answer"]
|
|
assert events[0]["route"] == "mode_profile_delete"
|
|
assert events[0]["engine"] == "n7web-mode-profile-manager"
|
|
assert events[-1]["route"] == "mode_profile_delete"
|
|
assert vehicle_service.mode_profiles.resolve_exact_name(
|
|
"阅读模式", "denza_n7"
|
|
) is None
|
|
client.start_thread.assert_not_called()
|
|
client.start_turn.assert_not_called()
|