Files
Cognitive_Agent/tests/integration/test_mode_creation_seat_policy.py
T

472 lines
21 KiB
Python

"""Creation selects one seat by default without remapping incompatible actuators."""
from __future__ import annotations
import json
import queue
from copy import deepcopy
from unittest.mock import MagicMock, patch
import pytest
from jsonschema import Draft202012Validator
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.codex_assistant import ActiveCodexTurn, 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 policy_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 / "artifacts"
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("physical CAN forbidden")) as reserve,
patch.object(service._vehicle_mqtt_transport, "bus", side_effect=AssertionError("physical MQTT forbidden")) as mqtt,
):
yield service, AssistantCapabilityCatalog(service), user_root
reserve.assert_not_called()
mqtt.assert_not_called()
finally:
service.close()
def _actions(result):
return (result.get("intent_ir") or {}).get("spec", {}).get("actions", [])
def _seat_scopes(result):
return {
action["target"]["instance"] for action in _actions(result)
if action["domain"] == "seat"
}
def _operation_key(operation):
target = operation.get("target_scope")
if target is None:
raw_target = operation.get("target") or {}
target = raw_target.get("instance") or raw_target.get("zone")
return (
operation["capability"], target,
json.dumps(operation["parameters"], ensure_ascii=False, sort_keys=True),
)
def _preview(service, message, *, plan=None, draft=None):
return service.preview_native_can(
message, provider="local", conversation_id="conv-seat-policy",
execution_mode="hardware", vehicle_id="denza_n7",
assistant_plan=plan, mode_profile_draft_context=draft,
)
def _assert_created(result, scopes):
assert result["status"] == "succeeded", result.get("validation")
assert result.get("mode_profile_draft"), result
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert _seat_scopes(result) == set(scopes)
schedule = result.get("schedule")
if schedule:
for phase in schedule["phases"]:
assert {
op["target_scope"] for op in phase["operations"] if op["domain"] == "seat"
} <= set(scopes)
assert result["mode_profile_draft"]["schedule"] == schedule
def _assistant_preview(service, catalog, message, *, client=None):
if client is None:
client = MagicMock()
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
fast = catalog.fast_answer(message, snapshot)
results, events = [], []
def execute(instruction, progress, assistant_plan=None):
result = _preview(service, instruction, plan=assistant_plan)
results.append(result)
return result
response = assistant.stream(
message, conversation_id="conv-seat-policy", principal="seat-policy-test", history=[],
authoritative_answer=fast.answer if fast else None,
emit=lambda event: events.append(event) or True,
execute_control=execute, 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(results) == 1, (response, results)
return response, results[0], client
@pytest.mark.parametrize("name", [
"阅读模式", "观影模式", "办公模式", "座椅舒适模式", "放松模式", "座椅动态模式", "上车预热模式",
])
@pytest.mark.parametrize(("prefix", "scopes"), [
("", {"passenger"}), ("主驾", {"driver"}), ("主驾和副驾", {"driver", "passenger"}),
])
def test_registered_modes_use_selected_seat_through_assistant_fast_path(policy_service, name, prefix, scopes):
service, catalog, user_root = policy_service
response, preview, client = _assistant_preview(service, catalog, f"构建一个{prefix}{name}")
assert response["success"] is True, (response, preview)
_assert_created(preview, scopes)
client.start_thread.assert_not_called()
client.start_turn.assert_not_called()
client.start_structured_turn.assert_not_called()
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("message", "scopes"), [
("构建一个两个座椅的阅读模式", {"driver", "passenger"}),
("构建一个两张座椅的阅读模式", {"driver", "passenger"}),
("构建一个两座椅的阅读模式", {"driver", "passenger"}),
("构建一个前排两座的阅读模式", {"driver", "passenger"}),
("构建一个副驾驶席阅读模式", {"passenger"}),
("构建一个副驾驶位阅读模式", {"passenger"}),
("构建一个副驾驶座椅阅读模式", {"passenger"}),
("构建一个两个座椅的动态模式", {"driver", "passenger"}),
("构建一个两张座椅的动态模式", {"driver", "passenger"}),
("构建一个两座椅的动态模式", {"driver", "passenger"}),
("构建一个前排两座的动态模式", {"driver", "passenger"}),
("构建一个副驾驶座椅动态模式", {"passenger"}),
])
def test_registered_seat_aliases_select_correct_targets_without_model(policy_service, message, scopes):
service, catalog, _ = policy_service
response, preview, client = _assistant_preview(service, catalog, message)
assert response["success"] is True, (
response["answer"], preview.get("validation"), preview.get("mode_profile_draft"),
)
_assert_created(preview, scopes)
assert response["route"] == "scene_template"
client.start_structured_turn.assert_not_called()
client.start_turn.assert_not_called()
@pytest.mark.parametrize("template_id", [
"reading", "cinema", "camping", "quiet-night", "reception", "romantic",
"winter-warmth", "summer-cooling", "long-trip-comfort", "relaxation", "seat-comfort",
"seat-dynamic", "boarding-warmup",
])
@pytest.mark.parametrize(("prefix", "scope"), [("", "passenger"), ("主驾", "driver")])
def test_trusted_template_selection_keeps_nonseat_devices_and_native_parameters(policy_service, template_id, prefix, scope):
service, _, _ = policy_service
template = service.scene_templates.get_template(template_id)
original = deepcopy(template.plan())
result = _preview(service, f"构建一个{prefix}{template.name}", plan=template.plan())
_assert_created(result, {scope})
raw_operations = (
[op for phase in original["schedule"]["phases"] for op in phase["operations"]]
if original.get("schedule") else [op.to_yaml() for op in template.operations]
)
expected = [
_operation_key(op) for op in raw_operations
if op["domain"] != "seat" or op["target_scope"] == scope
]
assert sorted(map(_operation_key, _actions(result))) == sorted(expected)
assert template.plan() == original, "projecting a request must not mutate the registered template"
def _model_output(scopes, *, dynamic=False):
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},
})
if not dynamic:
return {"requests": requests, "ambiguities": [], "confidence": 0.95}
return {
"requests": [], "ambiguities": [], "confidence": 0.95,
"schedule": {
"repeat_count": 2,
"phases": [
{"id": "relax", "name": "舒缓", "duration_ms": 30000, "requests": deepcopy(requests)},
{"id": "support", "name": "支撑", "duration_ms": 60000, "requests": deepcopy(requests)},
],
},
}
@pytest.mark.parametrize("dynamic", [False, True])
@pytest.mark.parametrize(("prefix", "scopes"), [
("", {"passenger"}), ("主驾", {"driver"}), ("主驾和副驾", {"driver", "passenger"}),
])
def test_unregistered_model_plan_preserves_correct_explicit_or_default_targets(policy_service, dynamic, prefix, scopes):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output(scopes, dynamic=dynamic), catalog.scene_composition_context(), dynamic=dynamic,
)
dynamic_label = "动态" if dynamic else ""
result = _preview(service, f"构建一个{prefix}星河释压{dynamic_label}模式", plan=plan)
_assert_created(result, scopes)
assert result["mode_profile_draft"]["generation_source"] == "codex_scene_composition"
@pytest.mark.parametrize("dynamic", [False, True])
@pytest.mark.parametrize("prefix", ["", "主驾", "副驾"])
def test_unregistered_model_cannot_invent_cross_seat_actions(policy_service, dynamic, prefix):
service, catalog, user_root = policy_service
plan = normalize_scene_output(
_model_output({"driver", "passenger"}, dynamic=dynamic),
catalog.scene_composition_context(), dynamic=dynamic,
)
try:
result = _preview(service, f"构建一个{prefix}星河释压{'动态' if dynamic else ''}模式", plan=plan)
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert result["hardware_execution_allowed"] is False
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("prefix", "requested_scope", "unexpected_scope"), [
("", "passenger", "driver"), ("主驾", "driver", "passenger"),
])
def test_unexpected_seat_only_in_later_dynamic_phase_rejects_entire_draft(
policy_service, prefix, requested_scope, unexpected_scope,
):
service, catalog, user_root = policy_service
output = _model_output({requested_scope}, dynamic=True)
output["schedule"]["phases"][1]["requests"] = _model_output({unexpected_scope})["requests"]
plan = normalize_scene_output(output, catalog.scene_composition_context(), dynamic=True)
try:
result = _preview(service, f"构建一个{prefix}星河释压动态模式", plan=plan)
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 service.mode_schedule_runner.status("denza_n7")["status"] == "idle"
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize("message", [
"构建一个办公模式,空调先24度持续30秒,然后24度持续60秒,循环2次",
"构建一个办公模式,主驾座椅分两个阶段动态舒缓",
])
def test_existing_mode_binding_cannot_authorize_cross_seat_model_schedule(policy_service, message):
service, catalog, user_root = policy_service
original = service.mode_profiles.get_profile("office", "denza_n7")
revision = service.mode_profiles.profile_revision(original)
plan = normalize_scene_output(
_model_output({"driver", "passenger"}, dynamic=True),
catalog.scene_composition_context(), dynamic=True,
)
try:
result = _preview(service, message, plan=plan)
except ValueError:
pass
else:
assert result["status"] != "succeeded", "binding an existing mode is not authorization for model-added seats"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert not result.get("command_plans")
assert service.mode_profiles.profile_revision(service.mode_profiles.get_profile("office", "denza_n7")) == revision
assert not list(user_root.rglob("*.yaml"))
@pytest.mark.parametrize(("message", "scope"), [
("构建一个办公模式,空调先24度持续30秒,然后24度持续60秒,循环2次", "passenger"),
("构建一个办公模式,主驾座椅分两个阶段动态舒缓", "driver"),
])
def test_existing_mode_binding_keeps_valid_selected_seat_schedule_saveable(policy_service, message, scope):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output({scope}, dynamic=True), catalog.scene_composition_context(), dynamic=True,
)
result = _preview(service, message, plan=plan)
_assert_created(result, {scope})
draft = result["mode_profile_draft"]
assert draft["existing_mode_id"] == "office"
assert draft["existing_mode_revision"]
saved = service.save_mode_profile_card(
draft["existing_mode_id"], draft["name"], draft["instruction"],
draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded"
assert saved["mode_profile"]["schedule"] == result["schedule"]
executed = service.execute_mode_profile_card(
saved["mode_profile"]["id"], execution_mode="dry_run", vehicle_id="denza_n7",
)
assert executed["status"] == "succeeded"
assert _seat_scopes(executed) == {scope}
@pytest.mark.parametrize("dynamic", [False, True])
def test_default_seat_preference_does_not_force_seat_actions_into_nonseat_scene(policy_service, dynamic):
service, catalog, _ = policy_service
plan = normalize_scene_output(
_model_output(set(), dynamic=dynamic), catalog.scene_composition_context(), dynamic=dynamic,
)
result = _preview(service, f"构建一个星河恒温{'动态' if dynamic else ''}模式", plan=plan)
_assert_created(result, set())
assert all(action["domain"] == "climate" for action in _actions(result))
def test_explicit_atomic_mode_requirements_default_unqualified_seat_to_passenger(policy_service):
service, _, _ = policy_service
result = _preview(service, "构建一个自定义支撑模式,座椅靠背调整到45%,空调24度")
_assert_created(result, {"passenger"})
assert {_operation_key(action) for action in _actions(result)} == {
("seat.set_backrest_recline", "passenger", '{"position_pct": 45}'),
# Only the seat default changes; the existing local HVAC zone stays driver.
("climate.set_temperature", "driver", '{"temperature_c": 24}'),
}
@pytest.mark.parametrize("dynamic", [False, True])
def test_passenger_position_capability_is_not_retargeted_to_driver(policy_service, dynamic):
service, catalog, _ = policy_service
output = _model_output({"passenger"}, dynamic=dynamic)
requests = output["schedule"]["phases"][0]["requests"] if dynamic else output["requests"]
requests[0]["target_scope"] = "driver"
try:
plan = normalize_scene_output(output, catalog.scene_composition_context(), dynamic=dynamic)
result = _preview(service, f"构建一个主驾星河释压{'动态' if dynamic else ''}模式", plan=plan)
except ValueError:
return
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
@pytest.mark.parametrize(("message", "scopes"), [
("构建一个主驾空调与座椅舒适模式", {"passenger"}),
("构建一个主驾阅读灯与座椅舒适模式", {"passenger"}),
("构建一个副驾空调与主驾座椅舒适模式", {"driver"}),
("构建一个主驾阅读灯与副驾座椅舒适模式", {"passenger"}),
])
def test_other_device_zones_do_not_select_or_broaden_seat_scope(policy_service, message, scopes):
service, catalog, _ = policy_service
plan = normalize_scene_output(_model_output(scopes), catalog.scene_composition_context())
result = _preview(service, message, plan=plan)
_assert_created(result, scopes)
@pytest.mark.parametrize(("template_id", "prefix", "scope"), [
("reading", "", "passenger"), ("reading", "主驾", "driver"),
("seat-dynamic", "", "passenger"), ("seat-dynamic", "主驾", "driver"),
("boarding-warmup", "", "passenger"), ("boarding-warmup", "主驾", "driver"),
])
def test_save_reload_and_simulated_execution_preserve_selected_seat(policy_service, template_id, prefix, scope):
service, _, user_root = policy_service
template = service.scene_templates.get_template(template_id)
preview = _preview(service, f"构建一个{prefix}{template.name}", plan=template.plan())
_assert_created(preview, {scope})
draft = preview["mode_profile_draft"]
saved = service.create_mode_profile_card(
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
)
assert saved["status"] == "succeeded", saved
service.mode_profiles = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes", user_root=user_root)
executed = service.execute_mode_profile_card(
saved["mode_profile"]["id"], conversation_id="conv-seat-policy-replay",
execution_mode="dry_run", vehicle_id="denza_n7",
)
assert executed["status"] == "succeeded", executed.get("validation")
assert executed["hardware_execution_allowed"] is False
assert _seat_scopes(executed) == {scope}
assert sorted(map(_operation_key, _actions(executed))) == sorted(map(_operation_key, _actions(preview)))
if preview.get("schedule"):
assert executed["schedule"] == preview["schedule"]
assert executed["schedule_execution"]["completed"] is True
assert all(
_seat_scopes(phase["result"]) <= {scope}
for phase in executed["schedule_execution"]["phase_results"]
)
@pytest.mark.parametrize(("initial_scope", "prefix"), [("driver", "主驾"), ("passenger", "")])
def test_unrelated_draft_edit_retains_original_seat_and_explicit_edit_may_extend_it(policy_service, initial_scope, prefix):
service, catalog, _ = policy_service
name = f"{prefix}星河释压模式"
built = _preview(
service, f"构建一个{name}",
plan=normalize_scene_output(_model_output({initial_scope}), catalog.scene_composition_context()),
)
_assert_created(built, {initial_scope})
draft = built["mode_profile_draft"]
revised = _preview(
service, f"把{name}的空调温度调整到26度",
draft={key: draft[key] for key in ("name", "source_request_id", "vehicle_id")},
)
_assert_created(revised, {initial_scope})
original_seat_ops = {_operation_key(a) for a in _actions(built) if a["domain"] == "seat"}
assert {_operation_key(a) for a in _actions(revised) if a["domain"] == "seat"} == original_seat_ops
added = "副驾座椅通风调整到1档" if initial_scope == "driver" else "主驾座椅通风调整到低档"
draft = revised["mode_profile_draft"]
extended = _preview(
service, f"把{name}的{added}",
draft={key: draft[key] for key in ("name", "source_request_id", "vehicle_id")},
)
_assert_created(extended, {"driver", "passenger"})
assert {_operation_key(a) for a in _actions(extended) if a["domain"] == "seat"} >= original_seat_ops
def _structured_client(output):
client = MagicMock()
client.available = True
client.status = "ready"
client.start_thread.return_value = "thread-seat-policy"
client.start_turn.side_effect = AssertionError("scene fell back to unconstrained agent")
def start(thread_id, text, *, application_context, output_schema, timeout=None):
Draft202012Validator(output_schema).validate(output)
turn = ActiveCodexTurn(thread_id=thread_id, turn_id="turn-seat-policy", events=queue.Queue())
turn.events.put({"method": "item/completed", "params": {"item": {
"id": "answer", "type": "agentMessage", "text": json.dumps(output, ensure_ascii=False),
}}})
turn.events.put({"method": "turn/completed", "params": {"turn": {
"id": turn.turn_id, "status": "completed", "items": [], "error": None,
}}})
return turn
client.start_structured_turn.side_effect = start
return client
@pytest.mark.parametrize(("prefix", "scopes"), [("", {"passenger"}), ("主驾", {"driver"})])
@pytest.mark.parametrize("dynamic", [False, True])
def test_unregistered_assistant_structured_path_uses_seat_policy(policy_service, prefix, scopes, dynamic):
service, catalog, _ = policy_service
client = _structured_client(_model_output(scopes, dynamic=dynamic))
response, preview, _ = _assistant_preview(
service, catalog, f"构建一个{prefix}星河释压{'动态' if dynamic else ''}模式", client=client,
)
assert response["success"] is True, (response, preview)
assert response["route"] == "scene_composer"
_assert_created(preview, scopes)
client.start_structured_turn.assert_called_once()
client.start_turn.assert_not_called()