"""A mode device list is an additive configuration edit, never live vehicle control.""" from __future__ import annotations import hashlib import json from copy import deepcopy from unittest.mock import MagicMock, patch import pytest from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog from cognitive_agent.application.assistant_plan_guard import MAX_PLAN_REQUESTS from cognitive_agent.application.codex_assistant import ( CodexAssistantService, _authorize_vehicle_request, ) from cognitive_agent.application.mode_profile import ModeProfileRegistry from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService @pytest.fixture def addition_service(tmp_path, monkeypatch): for variable in ( "COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED", "COGNITIVE_AGENT_VEHICLE_CAN_ENABLED", "COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED", "COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED", ): monkeypatch.setenv(variable, "false") 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: with ( patch.object(service.hardware_execution, "reserve", side_effect=AssertionError("mode edit reached real CAN")) as reserve, patch.object(service._vehicle_mqtt_transport, "bus", side_effect=AssertionError("mode edit reached MQTT")) as mqtt, patch.object(service.openai, "interpret", side_effect=AssertionError("deterministic mode edit contacted a model")), ): yield service, AssistantCapabilityCatalog(service), user_root reserve.assert_not_called() mqtt.assert_not_called() finally: service.close() def _preview(service, text, *, draft=None, plan=None, conversation="conv-device-additions"): return service.preview_native_can( text, provider="local", conversation_id=conversation, vehicle_id="denza_n7", execution_mode="hardware", mode_profile_draft_context=deepcopy(draft), assistant_plan=plan, ) def _actions(result): return (result.get("intent_ir") or {}).get("spec", {}).get("actions", []) def _action_map(result): return { (action["capability"], action["target"].get("instance") or action["target"].get("zone")): action["parameters"] for action in _actions(result) } def _profile_map(profile): return {(op.capability, op.target_scope): op.parameters for op in profile.operations} def _seat_actions(mapping): return {key: value for key, value in mapping.items() if key[0].startswith("seat.")} def _reference(result): return {key: result["mode_profile_draft"][key] for key in ("name", "source_request_id", "vehicle_id")} def _hashes(root): return {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in root.rglob("*.yaml")} def _seed_old_seat_only_draft(service, *, scope="passenger", name="躺平模式", extra=""): # A deliberately explicit, seat-only request represents an older legitimate # preview without relying on ignored developer artifacts or a model stub. settings = ( "副驾座椅水平向前调整到20%,副驾靠背调整到45%,副驾座椅坐盆调整到30%,副驾腿托调整到70%" if scope == "passenger" else "主驾座椅通风调整到低档,主驾座椅加热调整到低档" ) result = _preview(service, f"构建一个{name},{settings}{extra}") _assert_draft(result) assert {target for _, target in _seat_actions(_action_map(result))} == {scope} return result def _assert_draft(result): assert result["status"] == "succeeded", (result.get("mode_creation_failure"), result.get("validation")) assert result["orchestration_route"] == "mode_profile_create_preview" assert result["execution_mode"] == "dry_run" assert result["hardware_execution_allowed"] is False assert result.get("mode_profile_draft") assert len(_actions(result)) == result["mode_profile_draft"]["operation_count"] assert len(_actions(result)) <= MAX_PLAN_REQUESTS assert len(_action_map(result)) == len(_actions(result)), "append duplicated an existing action key" def _assistant(service, catalog, message, *, draft=None): client = MagicMock() client.available = False assistant = CodexAssistantService(client) snapshot = catalog.snapshot(vehicle_id="denza_n7") fast = catalog.fast_answer(message, snapshot) raw = [] def execute(instruction, progress, assistant_plan=None): result = _preview(service, instruction, draft=draft, plan=assistant_plan) raw.append(result) return result answer = assistant.stream( message, conversation_id="conv-device-additions", principal="device-additions-test", history=[], emit=lambda _event: True, execute_control=execute, authoritative_answer=fast.answer if fast else None, capability_snapshot=snapshot, capability_context=catalog.compact_context(snapshot), scene_capability_context=catalog.scene_composition_context(snapshot), control_terms=catalog.control_terms(snapshot), mode_profile_draft_context=draft, ) assert len(raw) == 1, answer client.start_structured_turn.assert_not_called() client.start_turn.assert_not_called() return answer, raw[0] @pytest.mark.parametrize("message", ["构建一个躺平模式", "帮我构建一个躺平模式吧", "构建一个主驾躺平模式"]) def test_flat_mode_fast_template_includes_requested_cabin_domains(addition_service, message): service, catalog, user_root = addition_service answer, result = _assistant(service, catalog, message) assert answer["success"] is True, answer assert answer["route"] == "scene_template" _assert_draft(result) assert {"seat", "climate", "ambient_light", "interior_light", "window"} <= { action["domain"] for action in _actions(result) } assert {target for _, target in _seat_actions(_action_map(result))} == ( {"driver"} if "主驾" in message else {"passenger"} ) assert not _hashes(user_root) @pytest.mark.parametrize("message", [ "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作", "帮我给躺平模式加上空调、氛围灯、阅读灯和车窗", "给躺平模式增加空调、氛围灯、阅读灯、车窗的联动", "躺平模式中补充空调、氛围灯、阅读灯和车窗动作", "调整躺平模式,加入空调、氛围灯、阅读灯、车窗", ]) @pytest.mark.parametrize("scope", ["passenger", "driver"]) def test_device_list_addition_revises_old_draft_without_execution_or_losing_seat_parameters(addition_service, message, scope): service, catalog, user_root = addition_service original = _seed_old_seat_only_draft(service, scope=scope) before = deepcopy(_action_map(original)) reference = _reference(original) authorization = _authorize_vehicle_request(message, mode_profile_draft_context=reference) assert authorization.reason == "explicit_mode_profile_draft_update" answer, revised = _assistant(service, catalog, message, draft=reference) assert answer["success"] is True, answer _assert_draft(revised) assert _seat_actions(_action_map(revised)) == _seat_actions(before) assert {"climate", "ambient_light", "interior_light", "window"} <= { action["domain"] for action in _actions(revised) } assert revised["mode_profile_draft"]["revises_source_request_id"] == original["request_id"] assert revised["mode_profile_draft"]["draft_revision"] == 2 assert _action_map(original) == before assert not _hashes(user_root) def test_repeated_device_list_is_idempotent_and_keeps_existing_custom_values(addition_service): service, _, user_root = addition_service original = _seed_old_seat_only_draft(service, extra=",空调调整到26度,关闭氛围灯") message = "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作" first = _preview(service, message, draft=_reference(original)) _assert_draft(first) for key, params in _action_map(original).items(): assert _action_map(first)[key] == params second = _preview(service, message, draft=_reference(first)) _assert_draft(second) assert _action_map(second) == _action_map(first) assert second["mode_profile_draft"]["draft_revision"] == 3 assert not _hashes(user_root) @pytest.mark.parametrize("scope", ["passenger", "driver"]) def test_saved_mode_addition_changes_only_selected_profile_and_can_be_replayed(addition_service, scope): service, _, user_root = addition_service original = _seed_old_seat_only_draft(service, scope=scope) draft = original["mode_profile_draft"] saved = service.create_mode_profile_card(draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"]) mode_id = saved["mode_profile"]["id"] unrelated = service.mode_profiles.save_mode("不会改变的模式", [{ "domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle", "parameters": {"temperature_c": 23}, }], vehicle_id="denza_n7") unrelated_revision = service.mode_profiles.profile_revision(unrelated) before = _profile_map(service.mode_profiles.get_profile(mode_id, "denza_n7")) updated = _preview(service, "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作") assert updated["status"] == "succeeded", updated.get("validation") assert updated["orchestration_route"] == "mode_profile_update" assert updated["execution_mode"] == "dry_run" assert updated["hardware_execution_allowed"] is False after = service.mode_profiles.get_profile(mode_id, "denza_n7") assert _seat_actions(_profile_map(after)) == _seat_actions(before) assert {"climate", "ambient_light", "interior_light", "window"} <= {op.domain for op in after.operations} assert service.mode_profiles.profile_revision(service.mode_profiles.get_profile(unrelated.id, "denza_n7")) == unrelated_revision assert len(_hashes(user_root)) == 2 replayed = service.execute_mode_profile_card(mode_id, execution_mode="dry_run", vehicle_id="denza_n7") assert replayed["status"] == "succeeded" assert _seat_actions(_action_map(replayed)) == _seat_actions(before) @pytest.mark.parametrize("mutation", ["request_id", "vehicle", "name", "conversation"]) def test_invalid_or_stale_context_never_becomes_live_control_or_changes_saved_mode(addition_service, mutation): service, _, user_root = addition_service original = _seed_old_seat_only_draft(service) reference = _reference(original) conversation = "conv-device-additions" if mutation == "request_id": reference["source_request_id"] = "req-missing-old-source" elif mutation == "vehicle": reference["vehicle_id"] = "other_vehicle" elif mutation == "name": reference["name"] = "其他模式" else: conversation = "conv-another-conversation" try: result = _preview(service, "在躺平模式中加入空调和车窗", draft=reference, conversation=conversation) except ValueError: pass else: assert result["status"] != "succeeded" assert not result.get("mode_profile_draft") assert result["execution_mode"] == "dry_run" assert result["hardware_execution_allowed"] is False assert not _actions(result) assert not result.get("command_plans") assert not _hashes(user_root) @pytest.mark.parametrize("message", [ "在躺平模式中加入空调和后备箱动作", "在躺平模式中加入阅读灯和天窗动作", "在躺平模式中加入空调并把温度调整到99度", ]) def test_unsupported_or_out_of_range_addition_rejects_all_changes_atomically(addition_service, message): service, _, user_root = addition_service original = _seed_old_seat_only_draft(service) try: result = _preview(service, message, draft=_reference(original)) except ValueError: pass else: assert result["status"] != "succeeded" assert not result.get("mode_profile_draft") assert not _actions(result) assert not result.get("command_plans") assert result["hardware_execution_allowed"] is False assert not _hashes(user_root) def test_adding_devices_to_full_twenty_action_draft_never_truncates_original_actions(addition_service): service, _, user_root = addition_service operations = [ ("climate.set_power", "vehicle", {"state": "on"}), ("climate.set_temperature", "vehicle", {"temperature_c": 24}), ("climate.set_fan_level", "vehicle", {"level": 2}), ("climate.set_cycle_mode", "vehicle", {"mode": "internal"}), ("climate.set_control_mode", "vehicle", {"mode": "auto"}), ("climate.set_front_defrost", "vehicle", {"state": "off"}), ("roof.set_shade_position", "vehicle", {"position_pct": 0}), ("window.set_position", "front_left", {"position_pct": 0}), ("window.set_position", "front_right", {"position_pct": 0}), ("ambient_light.set_power", "vehicle", {"state": "off"}), ("interior_light.set_indoor_power", "vehicle", {"state": "off"}), ("interior_light.set_reading_power", "all", {"state": "off"}), ("window.set_position", "rear_left", {"position_pct": 0}), ("window.set_position", "rear_right", {"position_pct": 0}), ("ambient_light.set_mode", "vehicle", {"mode": "romantic"}), ("ambient_light.set_brightness_level", "vehicle", {"brightness_level": 2}), ("ambient_light.set_color_index", "vehicle", {"color_index": 6}), ("seat.set_heating", "passenger", {"state": "on", "level": 1}), ("seat.set_backrest_recline", "passenger", {"position_pct": 45}), ("seat.set_legrest_height", "passenger", {"position_pct": 35}), ] plan = { "detected_domains": sorted({cap.split(".", 1)[0] for cap, _, _ in operations}), "requests": [{ "domain": cap.split(".", 1)[0], "capability": cap, "target_scope": target, "operation": "set", "parameters": [{"name": name, "value": value} for name, value in params.items()], "duration_ms": None, "reason": "固定目标边界回归。", } for cap, target, params in operations], "ambiguities": [], "confidence": 0.95, "decision_summary": ["验证20项原动作保留。"], } original = _preview(service, "构建一个满载参数模式", plan=plan) _assert_draft(original) assert len(_actions(original)) == 20 source_path = service.artifact_root / original["request_id"] / "result.json" source_hash = hashlib.sha256(source_path.read_bytes()).hexdigest() try: result = _preview(service, "在满载参数模式中加入副驾座椅通风动作", draft=_reference(original)) except ValueError: pass else: assert result["status"] != "succeeded" assert not result.get("mode_profile_draft") assert not _actions(result) assert hashlib.sha256(source_path.read_bytes()).hexdigest() == source_hash assert not _hashes(user_root) def test_previous_artifact_without_new_metadata_remains_an_editable_server_owned_draft(addition_service): service, _, user_root = addition_service original = _seed_old_seat_only_draft(service) legacy = deepcopy(original) legacy["mode_profile_draft"].pop("draft_revision", None) legacy["mode_profile_draft"].pop("generation_source", None) service._persist(legacy) result = _preview(service, "在躺平模式中加入空调、氛围灯、阅读灯、车窗", draft=_reference(legacy)) _assert_draft(result) assert result["mode_profile_draft"]["draft_revision"] == 2 assert _seat_actions(_action_map(result)) == _seat_actions(_action_map(original)) assert not _hashes(user_root) assert json.loads((service.artifact_root / original["request_id"] / "result.json").read_text(encoding="utf-8"))["request_id"] == original["request_id"]