Files
Cognitive_Agent/tests/unit/test_assistant_capability_catalog.py
T

691 lines
25 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.
from __future__ import annotations
import json
from collections import Counter
from collections.abc import Callable, Mapping
import pytest
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.generated.can_semantic_protocol import (
CAN0_SUPPORTED_CAPABILITIES,
CAN1_SUPPORTED_CAPABILITIES,
)
@pytest.fixture(scope="module")
def vehicle_service() -> SeatDemoService:
return SeatDemoService()
def _catalog(
vehicle_service: SeatDemoService,
*,
peripherals: Callable[[], list[dict[str, object]]] | None = None,
nfc: Callable[[], Mapping[str, object]] | None = None,
) -> AssistantCapabilityCatalog:
return AssistantCapabilityCatalog(
vehicle_service,
peripheral_provider=peripherals or list,
nfc_provider=nfc or (lambda: {"reader": {"state": "ready"}, "card": None}),
speech_provider=lambda: {"enabled": True, "status": "ready"},
gesture_provider=lambda: {"available": True, "status": "ready"},
calibration_provider=lambda: {"available": True, "status": "ready"},
self_repair_provider=lambda: {"ok": True, "status": "ready"},
)
def _by_id(snapshot: Mapping[str, object]) -> dict[str, dict[str, object]]:
return {
str(item["id"]): dict(item)
for item in snapshot["capabilities"]
if isinstance(item, Mapping)
}
def test_catalog_covers_every_authoritative_definition_exactly_once(
vehicle_service: SeatDemoService,
) -> None:
catalog = _catalog(vehicle_service)
snapshot = catalog.snapshot()
definitions = vehicle_service.capability_registry.definitions()
expected = {definition.name for definition in definitions}
actual = [item["id"] for item in snapshot["capabilities"]]
assert len(expected) == 44
assert len(actual) == len(set(actual)) == 44
assert set(actual) == expected
assert sum(bool(item["assistant_callable"]) for item in snapshot["capabilities"]) == 42
assert sum(bool(item["native_can_verified"]) for item in snapshot["capabilities"]) == 33
capabilities = _by_id(snapshot)
assert (
"#RRGGBB" in capabilities["ambient_light.set_illumination"]["parameters"][0]["description"]
)
def test_all_44_capabilities_preserve_authoritative_metadata(
vehicle_service: SeatDemoService,
) -> None:
"""The assistant projection must not silently drop or coerce registry facts."""
capabilities = _by_id(_catalog(vehicle_service).snapshot())
definitions = vehicle_service.capability_registry.definitions()
assert Counter(item["domain"] for item in capabilities.values()) == {
"ambient_light": 5,
"climate": 8,
"door": 2,
"interior_light": 3,
"outlet": 3,
"roof": 4,
"seat": 17,
"window": 2,
}
for definition in definitions:
item = capabilities[definition.name]
assert item["domain"] == definition.domain
assert item["description"]
assert item["target_devices"] == sorted(map(str, definition.spec["target_devices"]))
assert item["target_scopes"] == sorted(map(str, definition.spec["target_scopes"]))
assert item["risk_level"] == definition.spec["risk_level"]
assert item["confirmation_policy"] == definition.spec["confirmation_policy"]
assert item["declared_availability"] == [definition.availability]
assert item["catalog_ids"] == [definition.catalog_id]
assert item["executor_bindings"] == [definition.executor_binding]
projected_parameters = item["parameters"]
source_parameters = definition.spec.get("parameters", [])
assert [parameter["name"] for parameter in projected_parameters] == [
parameter["name"] for parameter in source_parameters
]
for projected, source in zip(projected_parameters, source_parameters, strict=True):
for field in ("name", "type", "unit", "required", "minimum", "maximum"):
if field in source:
assert projected[field] == source[field]
else:
assert field not in projected
if "allowed_values" in source:
assert projected["allowed_values"] == source["allowed_values"]
assert capabilities["door.set_vehicle_lock"]["execution_support"] == "observational_only"
assert capabilities["door.set_vehicle_lock"]["physical_control_supported"] is False
assert capabilities["door.set_rear_lock"]["execution_support"] == (
"matrix_candidate_with_feedback"
)
assert capabilities["door.set_rear_lock"]["physical_control_supported"] is True
def test_numeric_parameter_enums_remain_numeric_for_structured_planning(
vehicle_service: SeatDemoService,
) -> None:
capabilities = _by_id(_catalog(vehicle_service).snapshot())
def parameter(capability_id: str, name: str) -> dict[str, object]:
return next(
item for item in capabilities[capability_id]["parameters"] if item["name"] == name
)
assert parameter("seat.start_dynamic_mode", "mode_id")["allowed_values"] == [1, 2]
assert parameter("seat.set_driver_heating_native", "level")["allowed_values"] == [1, 2]
assert parameter("seat.set_driver_massage_native", "strength")["allowed_values"] == [
1,
2,
3,
]
assert all(
type(value) is int for value in parameter("seat.set_ventilation", "level")["allowed_values"]
)
def test_native_bindings_are_the_exact_can0_can1_union(
vehicle_service: SeatDemoService,
) -> None:
capabilities = _by_id(_catalog(vehicle_service).snapshot())
native = {
capability_id
for capability_id, capability in capabilities.items()
if capability["native_can_verified"] is True
}
assert native == set(CAN0_SUPPORTED_CAPABILITIES | CAN1_SUPPORTED_CAPABILITIES)
assert capabilities["seat.set_backrest_recline"]["verified_execution_backends"] == [
"passenger_seat_can0"
]
assert capabilities["roof.set_shade_position"]["verified_execution_backends"] == [
"vehicle_body_can1"
]
assert capabilities["seat.set_legrest_extension"]["native_can_verified"] is False
def test_catalog_does_not_promote_declared_only_controls(
vehicle_service: SeatDemoService,
) -> None:
capabilities = _by_id(_catalog(vehicle_service).snapshot())
assert capabilities["door.set_vehicle_lock"]["assistant_callable"] is False
assert capabilities["interior_light.set_wall_power"]["assistant_callable"] is False
assert capabilities["door.set_rear_lock"]["assistant_callable"] is True
assert capabilities["interior_light.set_reading_power"]["assistant_callable"] is True
non_callable = {
capability_id
for capability_id, item in capabilities.items()
if item["assistant_callable"] is not True
}
virtual_only = {
capability_id
for capability_id, item in capabilities.items()
if item["assistant_callable"] is True and item["native_can_verified"] is False
}
assert non_callable == {
"door.set_vehicle_lock",
"interior_light.set_wall_power",
}
assert virtual_only == {
"ambient_light.set_illumination",
"ambient_light.set_mode",
"climate.set_ac",
"climate.set_front_defrost",
"roof.set_automatic_dimming",
"roof.set_transmittance",
"seat.apply_posture_preset",
"seat.save_posture_preset",
"seat.set_legrest_extension",
}
def test_shade_and_smart_glass_are_distinct_execution_levels(
vehicle_service: SeatDemoService,
) -> None:
capabilities = _by_id(_catalog(vehicle_service).snapshot())
for capability_id in ("roof.set_shade_motion", "roof.set_shade_position"):
assert capabilities[capability_id]["assistant_callable"] is True
assert capabilities[capability_id]["native_can_verified"] is True
for capability_id in ("roof.set_transmittance", "roof.set_automatic_dimming"):
assert capabilities[capability_id]["assistant_callable"] is True
assert capabilities[capability_id]["native_can_verified"] is False
def test_all_modes_are_discoverable_and_reference_callable_capabilities(
vehicle_service: SeatDemoService,
) -> None:
snapshot = _catalog(vehicle_service).snapshot()
callable_ids = {
item["id"] for item in snapshot["capabilities"] if item["assistant_callable"] is True
}
modes = snapshot["modes"]
assert len(modes) == 13
assert {item["id"] for item in modes} >= {
"baby_nap",
"dynamic_cradle",
"office",
"rocking_horse",
"zero_gravity",
}
assert all(item["assistant_callable"] is True for item in modes)
assert all(set(item["operation_ids"]) <= callable_ids for item in modes)
assert (
next(item for item in modes if item["id"] == "baby_nap")[
"all_operations_have_native_binding"
]
is False
)
def test_user_mode_is_discovered_on_the_next_snapshot_without_restart(
vehicle_service: SeatDemoService,
monkeypatch: pytest.MonkeyPatch,
) -> None:
catalog = _catalog(vehicle_service)
before = catalog.snapshot()
vehicle_id = vehicle_service.vehicle_profiles.default().id
original_public_modes = vehicle_service.mode_profiles.public_modes
original_get_profile = vehicle_service.mode_profiles.get_profile
existing_profile = original_get_profile("nap", vehicle_id)
def public_modes(requested_vehicle_id: str) -> list[dict[str, object]]:
return [
*original_public_modes(requested_vehicle_id),
{
"id": "user_focus",
"name": "用户专注模式",
"aliases": ["专注一下"],
"description": "Runtime test mode.",
"domains": ["seat", "climate", "roof", "ambient_light"],
"operation_count": len(existing_profile.operations),
"origin": "user",
},
]
def get_profile(mode_id: str, requested_vehicle_id: str): # type: ignore[no-untyped-def]
if mode_id == "user_focus":
return existing_profile
return original_get_profile(mode_id, requested_vehicle_id)
monkeypatch.setattr(vehicle_service.mode_profiles, "public_modes", public_modes)
monkeypatch.setattr(vehicle_service.mode_profiles, "get_profile", get_profile)
after = catalog.snapshot()
mode = next(item for item in after["modes"] if item["id"] == "user_focus")
answer = catalog.fast_answer("当前有哪些模式", after)
assert len(before["modes"]) == 13
assert len(after["modes"]) == 14
assert after["catalog_revision"] != before["catalog_revision"]
assert mode["assistant_callable"] is True
assert answer is not None
assert "用户专注模式" in answer.answer
def test_mode_provider_failure_degrades_to_unknown_instead_of_empty(
vehicle_service: SeatDemoService,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def unavailable(_vehicle_id: str) -> list[dict[str, object]]:
raise RuntimeError("offline")
monkeypatch.setattr(vehicle_service.mode_profiles, "public_modes", unavailable)
catalog = _catalog(vehicle_service)
snapshot = catalog.snapshot()
answer = catalog.fast_answer("当前有哪些模式", snapshot)
assert snapshot["source_status"]["modes"] == "unavailable"
assert snapshot["modes"] == []
assert answer is not None
assert "暂时无法读取" in answer.answer
assert "0 个" not in answer.answer
def test_learned_peripheral_requires_enabled_capability_and_matching_nfc_card(
vehicle_service: SeatDemoService,
) -> None:
capability = {
"id": "lamp-on",
"status": "learned",
"enabled": True,
"device_id": "lamp-1",
"device_name": "闪灯玩具",
"object_names": ["闪烁灯", "闪灯玩具"],
"action": "开启",
"action_names": ["打开", "开启"],
"card_uid": "A1B2C3D4",
"slot": 7,
}
absent = _catalog(vehicle_service, peripherals=lambda: [capability]).snapshot()
absent_item = absent["learned_peripherals"][0]
assert absent_item["enabled"] is True
assert absent_item["active_for_current_nfc_card"] is False
assert absent_item["assistant_callable"] is False
assert "闪灯玩具" not in _catalog(
vehicle_service, peripherals=lambda: [capability]
).control_terms(absent)
present_catalog = _catalog(
vehicle_service,
peripherals=lambda: [capability],
nfc=lambda: {"reader": {"state": "online"}, "card": {"uid": "a1:b2-c3 d4"}},
)
present = present_catalog.snapshot()
present_item = present["learned_peripherals"][0]
assert present_item["active_for_current_nfc_card"] is True
assert present_item["assistant_callable"] is True
terms = present_catalog.control_terms(present)
assert "闪灯玩具" in terms
assert "闪烁灯" in terms
assert "打开" not in terms
compact_item = present_catalog.compact_context(present)["learned_peripherals"][0]
assert compact_item["status"] == "learned"
assert compact_item["assistant_callable"] is True
@pytest.mark.parametrize("status", ["learning", "failed", "", None])
def test_matching_nfc_never_promotes_an_unlearned_peripheral(
vehicle_service: SeatDemoService,
status: str | None,
) -> None:
peripheral = {
"id": "lamp-on",
"status": status,
"enabled": True,
"device_id": "lamp-1",
"device_name": "闪灯玩具",
"object_names": ["闪灯玩具"],
"action": "开启",
"action_names": ["打开"],
"card_uid": "A1B2",
"slot": 1,
}
catalog = _catalog(
vehicle_service,
peripherals=lambda: [peripheral],
nfc=lambda: {"card": {"uid": "A1B2"}},
)
snapshot = catalog.snapshot()
item = snapshot["learned_peripherals"][0]
assert item["active_for_current_nfc_card"] is True
assert item["assistant_callable"] is False
assert "闪灯玩具" not in catalog.control_terms(snapshot)
def test_live_infrared_catalog_and_nfc_presence_refresh_without_restart(
vehicle_service: SeatDemoService,
) -> None:
peripherals: list[dict[str, object]] = []
nfc: dict[str, object] = {"card": None}
catalog = _catalog(
vehicle_service,
peripherals=lambda: peripherals,
nfc=lambda: nfc,
)
before = catalog.snapshot()
peripherals.append(
{
"id": "lamp-on",
"status": "learned",
"enabled": True,
"device_id": "lamp-1",
"device_name": "闪灯玩具",
"object_names": ["闪灯玩具", "闪烁灯"],
"action": "开启",
"action_names": ["打开"],
"card_uid": "A1B2",
"slot": 1,
}
)
nfc["card"] = {"uid": "A1:B2"}
after = catalog.snapshot()
answer = catalog.fast_answer("当前有哪些红外外设", after)
assert before["learned_peripherals"] == []
assert before["catalog_revision"] != after["catalog_revision"]
assert after["learned_peripherals"][0]["assistant_callable"] is True
assert "闪灯玩具" in catalog.control_terms(after)
assert answer is not None
assert "闪灯玩具·开启" in answer.answer
peripherals[0]["enabled"] = False
disabled = catalog.snapshot()
assert disabled["learned_peripherals"][0]["assistant_callable"] is False
assert "闪灯玩具" not in catalog.control_terms(disabled)
@pytest.mark.parametrize(
("question", "required", "forbidden"),
[
("车端有哪些可控设备", ("空调", "遮阳帘", "座椅", "车窗", "车门锁", "出风口"), ()),
(
"支持遮阳帘吗",
("支持", "打开", "关闭", "半开", "0–100%", "0% 为完全关闭", "原生 CAN"),
("不支持",),
),
(
"遮阳帘位置范围是多少",
("0–100%", "0% 为完全关闭", "100% 为完全打开", "原生 CAN"),
("仅为虚拟编排",),
),
("支持整车锁车吗", ("不支持", "door.set_vehicle_lock"), ("已接入原生 CAN",)),
("支持整车解锁吗", ("不支持", "door.set_vehicle_lock"), ("已接入原生 CAN",)),
(
"支持后排车门解锁吗",
("支持后排车门上锁和解锁", "原生 CAN", "不代表支持整车"),
("door.set_vehicle_lock",),
),
(
"支持车门锁吗",
("仅后排车门锁", "整车锁车和整车解锁不可执行"),
("支持整车锁车",),
),
("支持壁灯吗", ("不支持", "interior_light.set_wall_power"), ("已接入原生 CAN",)),
("支持墙壁灯吗", ("不支持", "interior_light.set_wall_power"), ("已接入原生 CAN",)),
(
"支持阅读灯吗",
("支持阅读灯", "原生 CAN 只验证了全部阅读灯"),
("壁灯",),
),
(
"支持车内照明吗",
("室内灯和阅读灯可编排", "壁灯当前不可执行"),
(),
),
("可以打开天窗吗", ("不支持", "不包含物理天窗"), ("支持打开天窗",)),
("支持物理天窗吗", ("不支持", "不包含物理天窗"), ("支持打开天窗",)),
(
"支持智能天幕吗",
("支持工程编排", "0–100% 透光率", "自动调光", "没有已验证的物理 CAN"),
("已有原生 CAN",),
),
(
"天幕透光率范围是多少",
("0–100% 透光率", "当前仅为虚拟编排", "不包含物理天窗"),
("已有原生 CAN 绑定",),
),
(
"支持自动调光吗",
("支持工程编排", "自动调光", "没有已验证的物理 CAN"),
("已有原生 CAN",),
),
(
"支持主驾座椅按摩吗",
("支持主驾座椅按摩", "原生 CAN"),
("不支持主驾",),
),
(
"支持副驾座椅按摩吗",
("目前不支持副驾座椅按摩", "仅支持主驾"),
("支持副驾座椅按摩,已接入",),
),
(
"支持后排座椅加热吗",
("目前不支持后排座椅加热", "仅支持主驾和副驾"),
("支持后排座椅加热,已接入",),
),
(
"支持副驾座椅加热吗",
("支持副驾座椅加热", "原生 CAN"),
("不支持副驾",),
),
(
"支持前排座椅按摩吗",
("部分支持前排座椅按摩", "仅主驾可控", "副驾没有登记"),
("支持前排座椅按摩,",),
),
(
"支持后排座椅控制吗",
("目前不支持后排座椅控制", "仅支持主驾和副驾"),
("支持后排座椅控制,已接入",),
),
(
"支持座椅按摩吗",
("支持座椅按摩", "可控目标仅为主驾"),
("主驾与副驾座椅",),
),
(
"支持主驾阅读灯吗",
("不支持按单个座位", "只验证了全部阅读灯", "不会把局部请求扩大"),
("支持主驾阅读灯",),
),
(
"支持零重力模式吗",
("支持零重力模式", "4 项已登记工程能力"),
("不支持",),
),
(
"支持主驾零重力模式吗",
("目前不支持主驾零重力模式", "仅支持副驾"),
("支持主驾零重力模式,",),
),
(
"支持副驾零重力模式吗",
("支持零重力模式", "4 项已登记工程能力"),
("不支持副驾",),
),
(
"支持主驾座椅动态模式吗",
("目前不支持主驾座椅动态模式", "仅支持副驾"),
("支持主驾座椅动态模式,",),
),
(
"支持副驾座椅动态模式吗",
("支持副驾座椅动态模式", "工程能力"),
("不支持副驾",),
),
("支持大灯吗", ("不支持", "没有注册对应控制能力"), ("物理天窗",)),
],
)
def test_fast_answers_are_precise_capability_facts(
vehicle_service: SeatDemoService,
question: str,
required: tuple[str, ...],
forbidden: tuple[str, ...],
) -> None:
catalog = _catalog(vehicle_service)
answer = catalog.fast_answer(question, catalog.snapshot())
assert answer is not None
assert all(fragment in answer.answer for fragment in required)
assert all(fragment not in answer.answer for fragment in forbidden)
@pytest.mark.parametrize(
"instruction",
[
"打开遮阳帘",
"帮我打开遮阳帘好吗",
"可以把遮阳帘打开吗",
"把智能天幕透光率调到20%",
"请将后排车门上锁",
"请打开全部阅读灯",
"开启小憩模式",
],
)
def test_fast_answer_never_consumes_a_direct_control_request(
vehicle_service: SeatDemoService,
instruction: str,
) -> None:
catalog = _catalog(vehicle_service)
assert catalog.fast_answer(instruction, catalog.snapshot()) is None
def test_control_terms_do_not_authorize_declared_but_unexecutable_controls(
vehicle_service: SeatDemoService,
) -> None:
catalog = _catalog(vehicle_service)
terms = catalog.control_terms()
def authorized(instruction: str) -> bool:
normalized = "".join(instruction.lower().split())
return any("".join(term.lower().split()) in normalized for term in terms)
assert authorized("请锁车") is False
assert authorized("打开壁灯") is False
assert authorized("打开后排门锁") is True
assert authorized("打开阅读灯") is True
assert authorized("打开压缩机") is True
assert "锁车" not in terms
assert "解锁" not in terms
assert "壁灯" not in terms
def test_provider_failure_is_not_misreported_as_an_empty_catalog(
vehicle_service: SeatDemoService,
) -> None:
def unavailable() -> list[dict[str, object]]:
raise RuntimeError("offline")
catalog = _catalog(vehicle_service, peripherals=unavailable)
snapshot = catalog.snapshot()
answer = catalog.fast_answer("当前有哪些红外外设", snapshot)
assert snapshot["source_status"]["learned_peripherals"] == "unavailable"
assert answer is not None
assert "暂时无法读取" in answer.answer
assert "没有" not in answer.answer
def test_catalog_revision_is_stable_and_changes_with_live_capabilities(
vehicle_service: SeatDemoService,
) -> None:
first_catalog = _catalog(vehicle_service)
first = first_catalog.snapshot()
second = first_catalog.snapshot()
assert first["catalog_revision"] == second["catalog_revision"]
changed = _catalog(
vehicle_service,
peripherals=lambda: [
{
"id": "lamp-on",
"status": "learned",
"enabled": True,
"device_name": "闪灯玩具",
"object_names": ["闪灯玩具"],
"action": "开启",
"action_names": ["打开"],
"card_uid": "A1",
"slot": 1,
}
],
nfc=lambda: {"card": {"uid": "A1"}},
).snapshot()
assert changed["catalog_revision"] != first["catalog_revision"]
def test_model_projection_is_bounded_and_contains_no_internal_paths_or_tokens(
vehicle_service: SeatDemoService,
) -> None:
catalog = _catalog(vehicle_service)
snapshot = catalog.snapshot()
compact = catalog.compact_context(snapshot)
encoded = json.dumps(compact, ensure_ascii=False)
assert len(encoded.encode("utf-8")) < 128 * 1024
assert len(compact["capabilities"]) == 44
assert {
"id",
"domain",
"target_scopes",
"parameters",
"assistant_callable",
"native_can_verified",
} <= set(compact["capabilities"][0])
compact_by_id = {
item["id"]: item for item in compact["capabilities"] if isinstance(item, Mapping)
}
assert compact_by_id["roof.set_shade_position"]["target_scopes"] == ["vehicle"]
assert compact_by_id["roof.set_shade_position"]["parameters"] == [
{
"name": "position_pct",
"type": "integer",
"unit": "percent",
"minimum": 0,
"maximum": 100,
"required": True,
"description": "Absolute shade opening percentage.",
}
]
assert compact_by_id["roof.set_transmittance"]["native_can_verified"] is False
assert compact_by_id["door.set_vehicle_lock"]["assistant_callable"] is False
assert compact_by_id["interior_light.set_wall_power"]["assistant_callable"] is False
assert "roof.set_shade_motion" in encoded
assert "遮阳帘" in encoded
assert "D:\\Project" not in encoded
assert "approval_token" not in encoded
assert "raw_can" not in encoded
def test_filtered_query_returns_roof_facts_without_unrelated_capabilities(
vehicle_service: SeatDemoService,
) -> None:
catalog = _catalog(vehicle_service)
result = catalog.query(catalog.snapshot(), "遮阳帘")
assert result["matched"] is True
assert {item["id"] for item in result["device_groups"]} == {"roof"}
assert {item["domain"] for item in result["capabilities"]} == {"roof"}
assert "roof.set_shade_position" in {item["id"] for item in result["capabilities"]}