679 lines
26 KiB
Python
679 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections.abc import Callable, Mapping
|
|
from copy import deepcopy
|
|
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 ModeProfileRegistry
|
|
from cognitive_agent.application.scene_composer import normalize_scene_output
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
|
|
|
|
|
@pytest.fixture
|
|
def hybrid_service(
|
|
tmp_path: Path,
|
|
) -> tuple[SeatDemoService, AssistantCapabilityCatalog, Path]:
|
|
service = SeatDemoService(REPOSITORY_ROOT)
|
|
service.artifact_root = tmp_path / "runs"
|
|
user_root = tmp_path / "user-modes"
|
|
service.mode_profiles = ModeProfileRegistry(
|
|
REPOSITORY_ROOT / "config" / "modes",
|
|
user_root=user_root,
|
|
)
|
|
try:
|
|
yield service, AssistantCapabilityCatalog(service), user_root
|
|
finally:
|
|
service.close()
|
|
|
|
|
|
def _target(action: Mapping[str, object]) -> str:
|
|
target = action.get("target")
|
|
assert isinstance(target, Mapping)
|
|
return str(target.get("instance") or target.get("zone") or "")
|
|
|
|
|
|
def _actions(result: Mapping[str, object]) -> list[dict[str, object]]:
|
|
intent = result.get("intent_ir")
|
|
if isinstance(intent, Mapping):
|
|
spec = intent.get("spec")
|
|
raw_actions = spec.get("actions") if isinstance(spec, Mapping) else None
|
|
else:
|
|
raw_actions = result.get("actions")
|
|
assert isinstance(raw_actions, list)
|
|
assert all(isinstance(action, dict) for action in raw_actions)
|
|
return raw_actions
|
|
|
|
|
|
def _action_map(
|
|
result: Mapping[str, object],
|
|
) -> dict[tuple[str, str], dict[str, object]]:
|
|
return {
|
|
(str(action["capability"]), _target(action)): deepcopy(dict(action.get("parameters") or {}))
|
|
for action in _actions(result)
|
|
}
|
|
|
|
|
|
def _assert_default_control_bundle(result: Mapping[str, object]) -> None:
|
|
actions = _action_map(result)
|
|
assert actions["climate.set_power", "vehicle"] == {"state": "on"}
|
|
assert ("climate.set_temperature", "vehicle") in actions
|
|
assert ("climate.set_fan_level", "vehicle") in actions
|
|
assert ("climate.set_cycle_mode", "vehicle") in actions
|
|
for target in ("front_left", "front_right", "rear_left", "rear_right"):
|
|
assert actions["window.set_position", target] == {"position_pct": 0}
|
|
assert ("roof.set_shade_position", "vehicle") in actions
|
|
assert actions["seat.set_ventilation", "passenger"] == {
|
|
"state": "on",
|
|
"level": 1,
|
|
}
|
|
assert ("seat.set_backrest_recline", "passenger") in actions
|
|
assert ("seat.set_legrest_height", "passenger") in actions
|
|
assert any(
|
|
capability.startswith("ambient_light.")
|
|
for capability, _target in actions
|
|
)
|
|
|
|
|
|
def _draft_reference(result: Mapping[str, object]) -> dict[str, str]:
|
|
draft = result.get("mode_profile_draft")
|
|
assert isinstance(draft, Mapping)
|
|
return {
|
|
"name": str(draft["name"]),
|
|
"source_request_id": str(draft["source_request_id"]),
|
|
"vehicle_id": str(draft["vehicle_id"]),
|
|
}
|
|
|
|
|
|
def _template_hashes() -> dict[str, str]:
|
|
root = REPOSITORY_ROOT / "config" / "scene_templates"
|
|
return {
|
|
path.name: hashlib.sha256(path.read_bytes()).hexdigest()
|
|
for path in sorted(root.glob("*.yaml"))
|
|
}
|
|
|
|
|
|
def _assistant_preview(
|
|
service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
message: str,
|
|
*,
|
|
conversation_id: str,
|
|
) -> tuple[
|
|
dict[str, object],
|
|
dict[str, object],
|
|
list[dict[str, object]],
|
|
MagicMock,
|
|
list[dict[str, object] | None],
|
|
]:
|
|
client = MagicMock()
|
|
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] = []
|
|
|
|
def execute_control(
|
|
instruction: str,
|
|
progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object] | None = None,
|
|
) -> dict[str, object]:
|
|
plan = deepcopy(dict(assistant_plan)) if assistant_plan is not None else None
|
|
passed_plans.append(plan)
|
|
result = service.preview_native_can(
|
|
instruction,
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
on_commands_validated=progress,
|
|
assistant_plan=plan,
|
|
)
|
|
raw_results.append(result)
|
|
return result
|
|
|
|
response = assistant.stream(
|
|
message,
|
|
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,
|
|
capability_context=catalog.compact_context(snapshot),
|
|
scene_capability_context=catalog.scene_composition_context(snapshot),
|
|
control_terms=catalog.control_terms(snapshot),
|
|
)
|
|
assert len(raw_results) == 1
|
|
return response, raw_results[0], events, client, passed_plans
|
|
|
|
|
|
def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, user_root = hybrid_service
|
|
snapshot = catalog.snapshot(vehicle_id="denza_n7")
|
|
templates = snapshot["scene_templates"]
|
|
|
|
assert isinstance(templates, list)
|
|
assert len(templates) == 16
|
|
assert all(template["assistant_callable"] is True for template in templates)
|
|
|
|
with patch.object(
|
|
service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("a scene template preview reached hardware execution"),
|
|
):
|
|
for index, template in enumerate(templates):
|
|
plan = deepcopy(template["plan"])
|
|
result = service.preview_native_can(
|
|
f"构建一个{template['name']}",
|
|
provider="local",
|
|
conversation_id=f"conv-template-matrix-{index:02d}",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
assistant_plan=plan,
|
|
)
|
|
|
|
assert result["status"] == "succeeded", template["id"]
|
|
assert result["execution_mode"] == "dry_run", template["id"]
|
|
assert result["hardware_execution_allowed"] is False, template["id"]
|
|
assert result["orchestration_route"] == "mode_profile_create_preview"
|
|
draft = result["mode_profile_draft"]
|
|
assert draft["generation_source"] == "scene_template_copy"
|
|
assert draft["source_template_id"] == template["id"]
|
|
assert draft["source_template_revision"] == template["template_revision"]
|
|
assert "existing_mode_id" not in draft
|
|
|
|
expected_requests = plan["requests"]
|
|
if plan.get("schedule"):
|
|
expected_schedule = deepcopy(plan["schedule"])
|
|
for phase in expected_schedule["phases"]:
|
|
phase["operations"] = [
|
|
op for op in phase["operations"]
|
|
if op["domain"] != "seat" or op["target_scope"] == "passenger"
|
|
]
|
|
assert draft["schedule"] == expected_schedule
|
|
expected_requests = [
|
|
{**op, "parameters": [{"name": key, "value": value}
|
|
for key, value in op["parameters"].items()]}
|
|
for phase in expected_schedule["phases"] for op in phase["operations"]
|
|
]
|
|
else:
|
|
expected_requests = [
|
|
request for request in expected_requests
|
|
if request["domain"] != "seat" or request["target_scope"] == "passenger"
|
|
]
|
|
assert draft["operation_count"] == len(expected_requests)
|
|
expected = {
|
|
(
|
|
str(request["capability"]),
|
|
str(request["target_scope"]),
|
|
json.dumps(
|
|
{
|
|
parameter["name"]: parameter["value"]
|
|
for parameter in request["parameters"]
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
),
|
|
)
|
|
for request in expected_requests
|
|
}
|
|
actual = {
|
|
(
|
|
str(action["capability"]),
|
|
_target(action),
|
|
json.dumps(
|
|
action.get("parameters") or {},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
),
|
|
)
|
|
for action in _actions(result)
|
|
}
|
|
assert actual == expected, template["id"]
|
|
|
|
assert not list(user_root.rglob("*.yaml"))
|
|
|
|
|
|
def test_viewing_template_seeds_a_complete_unbound_preview_without_codex(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, user_root = hybrid_service
|
|
|
|
with patch.object(
|
|
service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("template copy reached hardware execution"),
|
|
):
|
|
response, raw_result, events, client, passed_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"构建一个观影模式",
|
|
conversation_id="conv-template-viewing-copy",
|
|
)
|
|
|
|
draft = raw_result["mode_profile_draft"]
|
|
assert response["route"] == "scene_template"
|
|
assert response["success"] is True
|
|
assert raw_result["status"] == "succeeded"
|
|
assert raw_result["execution_mode"] == "dry_run"
|
|
assert raw_result["hardware_execution_allowed"] is False
|
|
assert draft["name"] == "观影模式"
|
|
assert draft["generation_source"] == "codex_scene_composition"
|
|
assert "source_template_id" not in draft
|
|
assert "source_template_revision" not in draft
|
|
assert "existing_mode_id" not in draft
|
|
assert "existing_mode_revision" not in draft
|
|
assert "source_template_id" not in raw_result["mode_creation"]
|
|
assert "metadata" not in passed_plans[0]
|
|
_assert_default_control_bundle(raw_result)
|
|
assert events[0]["engine"] == "n7web-scene-template"
|
|
assert "创建阶段不会下发" in response["answer"]
|
|
assert not list(user_root.rglob("*.yaml"))
|
|
client.start_structured_turn.assert_not_called()
|
|
client.start_turn.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("case_id", "message", "expected_name", "template_id"),
|
|
(
|
|
(
|
|
"reading-purpose",
|
|
"帮我构建一个适合看书的功能模式",
|
|
"看书模式",
|
|
"reading",
|
|
),
|
|
(
|
|
"reading-purpose-preview",
|
|
"我想生成一个用来读书的模式并预览",
|
|
"读书模式",
|
|
"reading",
|
|
),
|
|
(
|
|
"relax-purpose",
|
|
"构建一个适合放松的功能模式",
|
|
"放松模式",
|
|
"relaxation",
|
|
),
|
|
(
|
|
"relax-purpose-polite",
|
|
"请生成用于放松的模式,先看看",
|
|
"放松模式",
|
|
"relaxation",
|
|
),
|
|
(
|
|
"cinema-purpose",
|
|
"帮我构建一个适合看电影的功能模式",
|
|
"看电影模式",
|
|
"cinema",
|
|
),
|
|
),
|
|
)
|
|
def test_purpose_descriptions_use_a_concise_name_and_zero_model_template_route(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
case_id: str,
|
|
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("purpose template reached hardware execution"),
|
|
):
|
|
response, raw_result, events, client, passed_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
message,
|
|
conversation_id=f"conv-template-{case_id}",
|
|
)
|
|
|
|
draft = raw_result["mode_profile_draft"]
|
|
assert response["route"] == "scene_template"
|
|
assert response["success"] is True
|
|
assert expected_name in response["answer"]
|
|
assert raw_result["status"] == "succeeded"
|
|
assert raw_result["execution_mode"] == "dry_run"
|
|
assert raw_result["hardware_execution_allowed"] is False
|
|
assert draft["name"] == expected_name
|
|
assert draft["generation_source"] == "codex_scene_composition"
|
|
assert "source_template_id" not in draft
|
|
assert draft["operation_count"] == len(_actions(raw_result))
|
|
assert "metadata" not in passed_plans[0]
|
|
_assert_default_control_bundle(raw_result)
|
|
assert events[0]["engine"] == "n7web-scene-template"
|
|
assert not list(user_root.rglob("*.yaml"))
|
|
client.start_structured_turn.assert_not_called()
|
|
client.start_turn.assert_not_called()
|
|
|
|
|
|
def test_explicit_settings_on_a_purpose_scene_bypass_template_defaults(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, _user_root = hybrid_service
|
|
response, raw_result, events, client, passed_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"构建一个适合看书的功能模式,空调温度调整到25度",
|
|
conversation_id="conv-template-purpose-explicit-settings",
|
|
)
|
|
|
|
draft = raw_result["mode_profile_draft"]
|
|
assert response["route"] == "vehicle_control"
|
|
assert response["success"] is True
|
|
assert draft["name"] == "看书模式"
|
|
assert draft["generation_source"] == "explicit_scene_requirements"
|
|
assert draft["operation_count"] == 1
|
|
assert passed_plans == [None]
|
|
assert events[0]["engine"] == "n7web-mode-builder"
|
|
client.start_structured_turn.assert_not_called()
|
|
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_only_passenger_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
|
|
if "座椅" in message:
|
|
assert result["mode_profile_draft"]["source_template_id"] == template_id
|
|
else:
|
|
assert result["mode_profile_draft"]["generation_source"] == (
|
|
"codex_scene_composition"
|
|
)
|
|
assert "source_template_id" not in result["mode_profile_draft"]
|
|
_assert_default_control_bundle(result)
|
|
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} == {"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_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_unscoped_seat_instruction_inside_mode_creation_defaults_to_passenger(
|
|
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 _action_map(result) == {
|
|
("seat.set_backrest_recline", "passenger"): {"position_pct": 45},
|
|
}
|
|
assert result.get("mode_profile_draft")
|
|
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:
|
|
service, catalog, user_root = hybrid_service
|
|
snapshot = catalog.snapshot(vehicle_id="denza_n7")
|
|
template = next(
|
|
item for item in snapshot["scene_templates"] if item["id"] == "cinema"
|
|
)
|
|
modified = deepcopy(template["plan"])
|
|
modified["requests"][0]["parameters"][0]["value"] = "off"
|
|
|
|
with pytest.raises(ValueError, match="模板已更新"):
|
|
service.preview_native_can(
|
|
"构建一个观影模式",
|
|
provider="local",
|
|
conversation_id="conv-template-plan-tamper",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
assistant_plan=modified,
|
|
)
|
|
|
|
stale = deepcopy(template["plan"])
|
|
stale["metadata"]["template_revision"] = "0123456789abcdef"
|
|
with pytest.raises(ValueError, match="模板已更新"):
|
|
service.preview_native_can(
|
|
"构建一个观影模式",
|
|
provider="local",
|
|
conversation_id="conv-template-plan-stale",
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
assistant_plan=stale,
|
|
)
|
|
|
|
assert not list(user_root.rglob("*.yaml"))
|
|
|
|
|
|
def test_template_alias_keeps_the_user_requested_draft_name(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, _user_root = hybrid_service
|
|
|
|
response, raw_result, _events, client, passed_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"创建一个影院模式",
|
|
conversation_id="conv-template-alias-name",
|
|
)
|
|
|
|
draft = raw_result["mode_profile_draft"]
|
|
assert response["route"] == "scene_template"
|
|
assert draft["name"] == "影院模式"
|
|
assert draft["generation_source"] == "codex_scene_composition"
|
|
assert "source_template_id" not in draft
|
|
assert "metadata" not in passed_plans[0]
|
|
_assert_default_control_bundle(raw_result)
|
|
client.start_structured_turn.assert_not_called()
|
|
client.start_turn.assert_not_called()
|
|
|
|
|
|
def test_template_copy_can_be_revised_twice_without_losing_sibling_actions(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, user_root = hybrid_service
|
|
conversation_id = "conv-template-viewing-revision-chain"
|
|
|
|
with patch.object(
|
|
service.hardware_execution,
|
|
"reserve",
|
|
side_effect=AssertionError("template draft revision reached hardware execution"),
|
|
):
|
|
_response, built, _events, _client, _plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"构建一个观影模式",
|
|
conversation_id=conversation_id,
|
|
)
|
|
shade_revised = service.preview_native_can(
|
|
"把观影模式的遮阳帘变成100%",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
mode_profile_draft_context=_draft_reference(built),
|
|
)
|
|
final = service.preview_native_can(
|
|
"把观影模式的空调温度调整到25度",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
execution_mode="hardware",
|
|
vehicle_id="denza_n7",
|
|
mode_profile_draft_context=_draft_reference(shade_revised),
|
|
)
|
|
|
|
built_actions = _action_map(built)
|
|
shade_actions = _action_map(shade_revised)
|
|
final_actions = _action_map(final)
|
|
shade_key = ("roof.set_shade_position", "vehicle")
|
|
temperature_key = ("climate.set_temperature", "vehicle")
|
|
|
|
assert shade_revised["status"] == final["status"] == "succeeded"
|
|
assert shade_revised["execution_mode"] == final["execution_mode"] == "dry_run"
|
|
assert shade_revised["hardware_execution_allowed"] is False
|
|
assert final["hardware_execution_allowed"] is False
|
|
assert len(built_actions) == len(shade_actions) == len(final_actions) == 16
|
|
assert built_actions[shade_key] == {"position_pct": 0}
|
|
assert shade_actions[shade_key] == final_actions[shade_key] == {"position_pct": 100}
|
|
assert built_actions[temperature_key] == shade_actions[temperature_key] == {"temperature_c": 24}
|
|
assert final_actions[temperature_key] == {"temperature_c": 25}
|
|
untouched_keys = set(built_actions) - {shade_key, temperature_key}
|
|
assert {key: final_actions[key] for key in untouched_keys} == {
|
|
key: built_actions[key] for key in untouched_keys
|
|
}
|
|
|
|
first_draft = built["mode_profile_draft"]
|
|
second_draft = shade_revised["mode_profile_draft"]
|
|
final_draft = final["mode_profile_draft"]
|
|
assert first_draft["draft_revision"] == 1
|
|
assert second_draft["draft_revision"] == 2
|
|
assert final_draft["draft_revision"] == 3
|
|
assert second_draft["generation_source"] == "draft_revision"
|
|
assert final_draft["generation_source"] == "draft_revision"
|
|
assert "source_template_id" not in second_draft
|
|
assert "source_template_id" not in final_draft
|
|
assert "existing_mode_id" not in final_draft
|
|
assert not list(user_root.rglob("*.yaml"))
|
|
|
|
|
|
def test_saving_a_template_copy_never_mutates_template_and_registered_mode_wins_next(
|
|
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
|
) -> None:
|
|
service, catalog, user_root = hybrid_service
|
|
hashes_before = _template_hashes()
|
|
template_revision = service.scene_templates.get_template("cinema").revision
|
|
|
|
_response, built, _events, first_client, first_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"构建一个观影模式",
|
|
conversation_id="conv-template-save-source",
|
|
)
|
|
draft = built["mode_profile_draft"]
|
|
saved = service.create_mode_profile_card(
|
|
str(draft["name"]),
|
|
str(draft["instruction"]),
|
|
str(draft["source_request_id"]),
|
|
str(draft["vehicle_id"]),
|
|
)
|
|
|
|
saved_profile = saved["mode_profile"]
|
|
assert saved["status"] == "succeeded"
|
|
assert saved_profile["name"] == "观影模式"
|
|
assert saved_profile["origin"] == "user"
|
|
assert saved_profile["id"] != "cinema"
|
|
assert list(user_root.rglob(f"{saved_profile['id']}.yaml"))
|
|
assert _template_hashes() == hashes_before
|
|
assert service.scene_templates.get_template("cinema").revision == template_revision
|
|
assert "metadata" not in first_plans[0]
|
|
_assert_default_control_bundle(built)
|
|
first_client.start_structured_turn.assert_not_called()
|
|
|
|
response, registered, events, second_client, passed_plans = _assistant_preview(
|
|
service,
|
|
catalog,
|
|
"构建一个观影模式",
|
|
conversation_id="conv-template-saved-mode-priority",
|
|
)
|
|
|
|
registered_draft = registered["mode_profile_draft"]
|
|
assert response["route"] == "vehicle_control"
|
|
assert response["success"] is True
|
|
assert events[0]["engine"] == "n7web-mode-builder"
|
|
assert passed_plans == [None]
|
|
assert registered_draft["generation_source"] == "registered_mode_profile"
|
|
assert registered_draft["existing_mode_id"] == saved_profile["id"]
|
|
assert "source_template_id" not in registered_draft
|
|
second_client.start_structured_turn.assert_not_called()
|
|
second_client.start_turn.assert_not_called()
|