Files
Cognitive_Agent/tests/integration/test_mode_device_additions.py
T

330 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"]