994 lines
36 KiB
Python
994 lines
36 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
from collections import Counter
|
||
from collections.abc import Callable, Mapping
|
||
from dataclasses import replace
|
||
|
||
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_snapshot_resolves_explicit_vehicle_and_keeps_no_argument_default(
|
||
vehicle_service: SeatDemoService,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
registry = vehicle_service.vehicle_profiles
|
||
original_get = registry.get
|
||
default_vehicle = original_get(None)
|
||
alternate_vehicle = replace(
|
||
default_vehicle,
|
||
id="alternate_vehicle",
|
||
name="另一测试车型",
|
||
)
|
||
requested_ids: list[str | None] = []
|
||
|
||
def get(vehicle_id: str | None): # type: ignore[no-untyped-def]
|
||
requested_ids.append(vehicle_id)
|
||
if vehicle_id == alternate_vehicle.id:
|
||
return alternate_vehicle
|
||
return original_get(vehicle_id)
|
||
|
||
monkeypatch.setattr(registry, "get", get)
|
||
|
||
default_snapshot = catalog.snapshot()
|
||
alternate_snapshot = catalog.snapshot(vehicle_id=alternate_vehicle.id)
|
||
|
||
assert requested_ids == [None, alternate_vehicle.id]
|
||
assert default_snapshot["vehicle"] == {
|
||
"id": default_vehicle.id,
|
||
"name": default_vehicle.name,
|
||
}
|
||
assert alternate_snapshot["vehicle"] == {
|
||
"id": alternate_vehicle.id,
|
||
"name": alternate_vehicle.name,
|
||
}
|
||
assert catalog.scene_composition_context(alternate_snapshot)["vehicle"] == (
|
||
alternate_snapshot["vehicle"]
|
||
)
|
||
|
||
|
||
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"]
|
||
|
||
# Runtime user modes are intentionally discovered without a restart, so
|
||
# the repository's 13 presets are a lower bound rather than a fixed total.
|
||
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_mode_catalog_projects_bounded_action_details_without_free_form_reasons(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
snapshot = _catalog(vehicle_service).snapshot()
|
||
office = next(item for item in snapshot["modes"] if item["id"] == "office")
|
||
|
||
assert len(office["operations"]) == office["operation_count"] == 12
|
||
temperature = next(
|
||
item
|
||
for item in office["operations"]
|
||
if item["capability"] == "climate.set_temperature"
|
||
)
|
||
assert temperature == {
|
||
"domain": "climate",
|
||
"capability": "climate.set_temperature",
|
||
"target_scope": "passenger",
|
||
"operation": "set",
|
||
"parameters": [{"name": "temperature_c", "value": 25}],
|
||
}
|
||
assert all("reason" not in operation for operation in office["operations"])
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"question",
|
||
(
|
||
"办公模式下会控制哪些车端设备,执行哪些动作?",
|
||
"办公模式包含什么配置",
|
||
"请展示一下办公模式的设备和参数",
|
||
"请告诉我办公模式下执行哪些动作",
|
||
),
|
||
)
|
||
def test_saved_mode_detail_fast_answer_is_exact_read_only_and_natural(
|
||
vehicle_service: SeatDemoService,
|
||
question: str,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
answer = catalog.fast_answer(question, catalog.snapshot())
|
||
|
||
assert answer is not None
|
||
assert answer.topic == "specific_mode_detail"
|
||
assert all(
|
||
fragment in answer.answer
|
||
for fragment in (
|
||
"办公模式",
|
||
"座椅",
|
||
"空调",
|
||
"遮阳帘",
|
||
"氛围灯",
|
||
"副驾空调温度设为25℃",
|
||
"共12项动作",
|
||
"本次仅查询,未下发车辆控制",
|
||
)
|
||
)
|
||
assert "Matched mode profile" not in answer.answer
|
||
assert "climate.set_temperature" not in answer.answer
|
||
|
||
|
||
def test_unknown_mode_detail_fast_answer_fails_closed_without_inventing_actions(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
answer = catalog.fast_answer(
|
||
"火星模式中包含哪些设备和动作?",
|
||
catalog.snapshot(),
|
||
)
|
||
|
||
assert answer is not None
|
||
assert answer.topic == "specific_mode_detail_not_found"
|
||
assert "火星模式" in answer.answer
|
||
assert "未找到" in answer.answer
|
||
assert "未下发车辆控制" in answer.answer
|
||
|
||
|
||
def test_common_scene_templates_are_read_only_callable_catalog_entries(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
snapshot = _catalog(vehicle_service).snapshot()
|
||
templates = snapshot["scene_templates"]
|
||
|
||
assert snapshot["source_status"]["scene_templates"] == "ready"
|
||
assert len(templates) == 15
|
||
assert {item["name"] for item in templates} >= {
|
||
"观影模式",
|
||
"露营模式",
|
||
"阅读模式",
|
||
"放松模式",
|
||
"雨天除雾模式",
|
||
"夏季清凉模式",
|
||
"冬季温暖模式",
|
||
}
|
||
assert all(item["read_only"] is True for item in templates)
|
||
assert all(item["assistant_callable"] is True for item in templates)
|
||
assert all(item["plan"]["metadata"]["template_id"] == item["id"] for item in templates)
|
||
assert all("source" not in item for item in templates)
|
||
|
||
|
||
def test_scene_template_provider_failure_is_explicit_and_empty(
|
||
vehicle_service: SeatDemoService,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
def unavailable(*_args: object, **_kwargs: object) -> list[dict[str, object]]:
|
||
raise OSError("template directory unavailable")
|
||
|
||
monkeypatch.setattr(vehicle_service.scene_templates, "public_templates", unavailable)
|
||
snapshot = _catalog(vehicle_service).snapshot()
|
||
|
||
assert snapshot["source_status"]["scene_templates"] == "unavailable"
|
||
assert snapshot["scene_templates"] == []
|
||
|
||
|
||
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(after["modes"]) == len(before["modes"]) + 1
|
||
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
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"instruction",
|
||
[
|
||
"打开后备箱",
|
||
"请开启后备箱门",
|
||
"帮我把尾门打开一下",
|
||
"打开尾门盖",
|
||
"开启后备箱盖",
|
||
"麻烦帮忙打开电动尾门",
|
||
"关闭行李箱",
|
||
"把行李厢盖关上",
|
||
"打开物理天窗",
|
||
"开启近光灯",
|
||
"打开前大灯",
|
||
"关闭后雾灯",
|
||
"请锁车",
|
||
"关闭墙壁灯",
|
||
"打开方向盘加热",
|
||
"调节方向盘",
|
||
"打开雨刮",
|
||
"请鸣笛",
|
||
"按喇叭",
|
||
"可以帮我打开后备箱吗",
|
||
"能不能打开尾门",
|
||
"后备箱能开吗",
|
||
"支持后备箱控制吗",
|
||
"是否可以打开后备箱",
|
||
"是否能打开后备箱",
|
||
"请问后备箱是否能打开",
|
||
],
|
||
)
|
||
def test_known_unsupported_controls_return_catalog_backed_quick_suggestions(
|
||
vehicle_service: SeatDemoService,
|
||
instruction: str,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
snapshot = catalog.snapshot()
|
||
callable_ids = {
|
||
item["id"] for item in snapshot["capabilities"] if item["assistant_callable"] is True
|
||
}
|
||
native_ids = {
|
||
item["id"] for item in snapshot["capabilities"] if item["native_can_verified"] is True
|
||
}
|
||
|
||
answer = catalog.fast_answer(instruction, snapshot)
|
||
|
||
assert answer is not None
|
||
assert answer.topic == "unsupported_vehicle_control"
|
||
assert answer.answer.startswith(
|
||
"当前车端暂不支持该功能控制哦,您可以尝试以下控制:"
|
||
)
|
||
assert 1 <= len(answer.suggestions) <= 3
|
||
assert all(suggestion.label in answer.answer for suggestion in answer.suggestions)
|
||
assert all(
|
||
set(suggestion.capability_ids) <= callable_ids for suggestion in answer.suggestions
|
||
)
|
||
assert all(set(suggestion.capability_ids) <= native_ids for suggestion in answer.suggestions)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"message",
|
||
[
|
||
"后备箱怎么打开?",
|
||
"不要打开后备箱",
|
||
"先别打开后备箱",
|
||
"如果打开后备箱会怎么样?",
|
||
"后备箱打不开",
|
||
"打开后备箱失败了",
|
||
"解释一下“打开后备箱”",
|
||
"帮我写一个打开后备箱的故事",
|
||
"今天聊聊后备箱",
|
||
"把后备箱打开到一半",
|
||
"可以介绍一下后备箱吗",
|
||
"你支持回答后备箱相关问题吗",
|
||
"哪些车型支持打开后备箱",
|
||
"支持后备箱保养知识问答吗",
|
||
],
|
||
)
|
||
def test_unsupported_control_fast_path_does_not_consume_non_commands(
|
||
vehicle_service: SeatDemoService,
|
||
message: str,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
assert catalog.fast_answer(message, catalog.snapshot()) is None
|
||
|
||
|
||
def test_unsupported_control_fast_path_leaves_mixed_supported_control_for_full_route(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
|
||
assert catalog.fast_answer("打开后备箱并关闭所有车窗", catalog.snapshot()) is None
|
||
assert catalog.fast_answer("打开天窗遮阳帘", catalog.snapshot()) is None
|
||
|
||
|
||
def test_unsupported_control_fast_path_stands_down_when_runtime_adds_capability(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
snapshot = json.loads(json.dumps(catalog.snapshot(), ensure_ascii=False))
|
||
snapshot["capabilities"].append(
|
||
{
|
||
"id": "body.toggle_tailgate",
|
||
"domain": "door",
|
||
"description": "Toggle the vehicle tailgate.",
|
||
"target_devices": ["tailgate"],
|
||
"assistant_callable": True,
|
||
"target_scopes": ["vehicle"],
|
||
}
|
||
)
|
||
|
||
assert catalog.fast_answer("打开后备箱", snapshot) is None
|
||
|
||
|
||
def test_quick_control_suggestions_are_filtered_by_current_callable_catalog(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
snapshot = json.loads(json.dumps(catalog.snapshot(), ensure_ascii=False))
|
||
for capability in snapshot["capabilities"]:
|
||
if capability["id"] in {"window.set_motion", "roof.set_shade_motion"}:
|
||
capability["assistant_callable"] = False
|
||
|
||
answer = catalog.fast_answer("打开后备箱", snapshot)
|
||
|
||
assert answer is not None
|
||
assert [suggestion.id for suggestion in answer.suggestions] == ["set-climate-24c"]
|
||
assert "关闭所有车窗" not in answer.answer
|
||
assert "关闭遮阳帘" not in answer.answer
|
||
|
||
|
||
def test_quick_control_suggestions_require_native_can_verification(
|
||
vehicle_service: SeatDemoService,
|
||
) -> None:
|
||
catalog = _catalog(vehicle_service)
|
||
snapshot = json.loads(json.dumps(catalog.snapshot(), ensure_ascii=False))
|
||
for capability in snapshot["capabilities"]:
|
||
if capability["id"] == "climate.set_temperature":
|
||
capability["native_can_verified"] = False
|
||
|
||
answer = catalog.fast_answer("打开后备箱", snapshot)
|
||
|
||
assert answer is not None
|
||
assert "set-climate-24c" not in {item.id for item in answer.suggestions}
|
||
assert all(
|
||
capability_id != "climate.set_temperature"
|
||
for item in answer.suggestions
|
||
for capability_id in item.capability_ids
|
||
)
|
||
|
||
|
||
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"]}
|