from __future__ import annotations import os import queue from collections.abc import Callable, Iterator, Mapping from dataclasses import dataclass from pathlib import Path import pytest from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog from cognitive_agent.application.assistant_plan_guard import guard_assistant_plan from cognitive_agent.application.codex_assistant import ( ActiveCodexTurn, CodexAssistantService, CodexVehicleToolError, ) from cognitive_agent.application.seat_service import SeatDemoService ROOT = Path(__file__).resolve().parents[2] REPOSITORY_MODE_IDS = frozenset( path.stem for path in (ROOT / "config" / "modes" / "vehicles" / "denza_n7").glob( "*.yaml" ) ) def _repository_catalog_modes(modes: object) -> list[Mapping[str, object]]: assert REPOSITORY_MODE_IDS assert isinstance(modes, list) mode_ids = { str(mode.get("id") or "") for mode in modes if isinstance(mode, Mapping) } assert "" not in mode_ids assert len(mode_ids) == len(modes), "catalog mode ids must remain unique" assert REPOSITORY_MODE_IDS <= mode_ids repository_modes = [ mode for mode in modes if isinstance(mode, Mapping) and str(mode.get("id") or "") in REPOSITORY_MODE_IDS ] assert {str(mode["id"]) for mode in repository_modes} == REPOSITORY_MODE_IDS return repository_modes @dataclass(frozen=True) class _CapabilityCase: capability: str message: str target: str parameters: Mapping[str, object] operation: str = "set" expected_effect: str = "action" @property def domain(self) -> str: return self.capability.split(".", 1)[0] def assistant_plan(self) -> dict[str, object]: return { "detected_domains": [self.domain], "requests": [ { "domain": self.domain, "capability": self.capability, "target_scope": self.target, "operation": self.operation, "duration_ms": None, "parameters": [ {"name": name, "value": value} for name, value in self.parameters.items() ], "reason": "将用户明确表达的控制目标规范化为登记能力。", } ], "ambiguities": [], "decision_summary": ["仅规范化原始请求明确涉及的能力。"], "confidence": 0.95, } CAPABILITY_CASES = ( _CapabilityCase( "ambient_light.set_brightness_level", "氛围灯亮度调到3档", "vehicle", {"brightness_level": 3}, ), _CapabilityCase( "ambient_light.set_color_index", "设置氛围灯颜色编号32", "vehicle", {"color_index": 32}, ), _CapabilityCase( "ambient_light.set_illumination", "氛围灯调成蓝色,亮度50%", "vehicle", {"color_hex": "#0000FF", "brightness_pct": 50}, ), _CapabilityCase( "ambient_light.set_mode", "氛围灯设置为影院模式", "vehicle", {"mode": "cinema"}, ), _CapabilityCase( "ambient_light.set_power", "打开氛围灯", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "climate.set_ac", "空调压缩机设为开启状态", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "climate.set_control_mode", "空调设置为自动控制模式", "vehicle", {"mode": "auto"}, ), _CapabilityCase( "climate.set_cycle_mode", "空调设置为内循环", "vehicle", {"mode": "internal"}, ), _CapabilityCase( "climate.set_fan_level", "空调风量调到3档", "vehicle", {"level": 3}, ), _CapabilityCase( "climate.set_front_defrost", "打开前挡风玻璃除雾", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "climate.set_outlet_mode", "空调出风模式设置为吹面吹脚", "vehicle", {"mode": "face_feet"}, ), _CapabilityCase( "climate.set_power", "打开空调", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "climate.set_temperature", "空调温度调到23度", "vehicle", {"temperature_c": 23}, ), _CapabilityCase( "door.set_rear_lock", "后门锁上锁", "rear", {"state": "lock"}, ), _CapabilityCase( "interior_light.set_indoor_power", "打开车内灯", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "interior_light.set_reading_power", "打开所有阅读灯", "all", {"state": "on"}, "activate", ), _CapabilityCase( "outlet.set_calibrated_position", "主驾左风口标定坐标水平-20垂直30", "driver_left", {"horizontal_value": -20, "vertical_value": 30}, ), _CapabilityCase( "outlet.set_position_percent", "主驾左风口水平25%垂直75%", "driver_left", {"horizontal_pct": 25, "vertical_pct": 75}, ), _CapabilityCase( "outlet.set_switch_state", "打开主驾左出风口", "driver_left", {"state": "open"}, ), _CapabilityCase( "roof.set_automatic_dimming", "开启智能天幕自动调光", "vehicle", {"state": "on"}, "activate", ), _CapabilityCase( "roof.set_shade_motion", "打开遮阳帘", "vehicle", {"motion": "open"}, ), _CapabilityCase( "roof.set_shade_position", "遮阳帘打开30%", "vehicle", {"position_pct": 30}, ), _CapabilityCase( "roof.set_transmittance", "智能天幕透光率调到30%", "vehicle", {"transmittance_pct": 30}, ), _CapabilityCase( "seat.apply_posture_preset", "调用座椅预设矩阵姿态", "passenger", {"preset_name": "矩阵姿态"}, "apply", "preset_apply", ), _CapabilityCase( "seat.recall_driver_memory_native", "召回主驾座椅2号记忆位置", "driver", {"slot": 2}, "apply", ), _CapabilityCase( "seat.save_posture_preset", "保存当前座椅姿态为矩阵姿态", "passenger", {"preset_name": "矩阵姿态"}, "save", "preset_save", ), _CapabilityCase( "seat.set_backrest_recline", "副驾靠背调到120度", "passenger", {"angle_deg": 120}, ), _CapabilityCase( "seat.set_cushion_tilt", "副驾座椅坐垫高度调到40%", "passenger", {"position_pct": 40}, ), _CapabilityCase( "seat.set_driver_axis_native", "主驾座椅水平向前移动500毫秒", "driver", {"axis": "horizontal", "direction": "forward", "duration_ms": 500}, ), _CapabilityCase( "seat.set_driver_heating_native", "打开主驾座椅加热2档", "driver", {"state": "on", "level": 2}, "activate", ), _CapabilityCase( "seat.set_driver_massage_native", "主驾座椅按摩强度开到3档", "driver", {"action": "strength", "strength": 3}, ), _CapabilityCase( "seat.set_driver_position_native", "主驾座椅记忆位置调到2号", "driver", {"slot": 2}, "apply", ), _CapabilityCase( "seat.set_driver_ventilation_native", "打开主驾座椅通风2档", "driver", {"state": "on", "level": 2}, "activate", ), _CapabilityCase( "seat.set_heating", "打开副驾座椅加热3档", "passenger", {"state": "on", "level": 3}, "activate", ), _CapabilityCase( "seat.set_legrest_extension", "副驾腿托伸出到40%", "passenger", {"position_pct": 40}, ), _CapabilityCase( "seat.set_legrest_height", "副驾腿托升高到40%", "passenger", {"position_pct": 40}, ), _CapabilityCase( "seat.set_longitudinal_position", "副驾座椅纵向调到40%", "passenger", {"position_pct": 40}, ), _CapabilityCase( "seat.set_ventilation", "打开副驾座椅通风3档", "passenger", {"state": "on", "level": 3}, "activate", ), _CapabilityCase( "seat.start_dynamic_mode", "启动副驾木马模式", "passenger", {"mode_id": 1}, "activate", ), _CapabilityCase( "seat.stop_dynamic_mode", "停止副驾木马模式", "passenger", {}, "deactivate", ), _CapabilityCase( "window.set_motion", "打开左前车窗", "front_left", {"motion": "open"}, ), _CapabilityCase( "window.set_position", "把左前车窗开到50%", "front_left", {"position_pct": 50}, ), ) def _complete_turn(turn: ActiveCodexTurn, answer: str = "已按要求完成。") -> None: item_id = f"answer-{turn.turn_id}" turn.events.put( { "method": "item/started", "params": { "item": { "type": "agentMessage", "id": item_id, "text": "", "phase": "final_answer", } }, } ) turn.events.put( { "method": "item/agentMessage/delta", "params": {"itemId": item_id, "delta": answer}, } ) turn.events.put( { "method": "item/completed", "params": { "item": { "type": "agentMessage", "id": item_id, "text": answer, "phase": "final_answer", } }, } ) turn.events.put( { "method": "turn/completed", "params": { "turn": { "id": turn.turn_id, "status": "completed", "items": [], "error": None, } }, } ) class _ToolCallingCodexClient: def __init__(self, *, available: bool = True) -> None: self.available = available self.on_turn: Callable[[str, ActiveCodexTurn], None] | None = None self.thread_calls = 0 self.turn_calls = 0 self.request_tokens: list[str] = [] @property def status(self) -> str: return "ready" if self.available else "failed" def start(self, _callback_url: str, _callback_token: str) -> None: return None def close(self) -> None: return None def start_thread(self, *, developer_instructions: str, ephemeral: bool = True) -> str: assert "get_n7web_capabilities" in developer_instructions assert "execute_cockpit_request" in developer_instructions assert ephemeral is True self.thread_calls += 1 return f"thread-matrix-{self.thread_calls}" def start_turn( self, thread_id: str, text: str, request_token: str, *, application_context: dict[str, object] | None = None, ) -> ActiveCodexTurn: del text assert thread_id.startswith("thread-matrix-") assert isinstance(application_context, dict) self.turn_calls += 1 self.request_tokens.append(request_token) turn = ActiveCodexTurn( thread_id=thread_id, turn_id=f"turn-matrix-{self.turn_calls}", events=queue.Queue(), ) if self.on_turn is not None: self.on_turn(request_token, turn) _complete_turn(turn) return turn def finish_turn(self, _turn: ActiveCodexTurn) -> None: return None def interrupt(self, _turn: ActiveCodexTurn) -> None: return None def unsubscribe_thread(self, _thread_id: str) -> None: return None @pytest.fixture(scope="module") def vehicle_service( tmp_path_factory: pytest.TempPathFactory, ) -> Iterator[SeatDemoService]: memory_path = ROOT / "tests" / "fixtures" / "driver_seat_memory.json" previous_memory_path = os.environ.get("COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH") os.environ["COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH"] = str(memory_path) service = SeatDemoService(ROOT) service.artifact_root = Path(tmp_path_factory.mktemp("assistant-matrix-runs")) yield service service.close() if previous_memory_path is None: os.environ.pop("COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH", None) else: os.environ["COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH"] = previous_memory_path @pytest.fixture(scope="module") def catalog(vehicle_service: SeatDemoService) -> AssistantCapabilityCatalog: return AssistantCapabilityCatalog( vehicle_service, peripheral_provider=list, nfc_provider=lambda: {"reader": {"state": "ready"}, "card": None}, ) def _stream_with_catalog( assistant: CodexAssistantService, catalog: AssistantCapabilityCatalog, message: str, *, execute_control: Callable[..., dict[str, object]], conversation_id: str, authoritative_answer: str | None = None, ) -> tuple[dict[str, object], list[dict[str, object]]]: snapshot = catalog.snapshot() events: list[dict[str, object]] = [] result = assistant.stream( message, conversation_id=conversation_id, principal="assistant-matrix-user", history=[], emit=lambda event: events.append(event), execute_control=execute_control, capability_snapshot=snapshot, capability_context=catalog.compact_context(snapshot), control_terms=catalog.control_terms(snapshot), query_capabilities=catalog.query, authoritative_answer=authoritative_answer, ) return result, events def test_execution_matrix_exactly_matches_every_assistant_callable_capability( catalog: AssistantCapabilityCatalog, ) -> None: callable_capabilities = { capability["id"] for capability in catalog.snapshot()["capabilities"] if capability["assistant_callable"] is True } matrix_capabilities = {case.capability for case in CAPABILITY_CASES} assert len(CAPABILITY_CASES) == len(matrix_capabilities), ( "the execution matrix must contain exactly one case per atomic capability" ) assert len(callable_capabilities) == 42 assert matrix_capabilities == callable_capabilities @pytest.mark.parametrize( "case", CAPABILITY_CASES, ids=lambda case: case.capability, ) def test_every_assistant_callable_capability_reaches_the_real_tool_boundary( vehicle_service: SeatDemoService, catalog: AssistantCapabilityCatalog, case: _CapabilityCase, ) -> None: proposed_plan = case.assistant_plan() guarded = guard_assistant_plan( proposed_plan, fallback_text=case.message, capability_context=vehicle_service.capability_context, executable_capabilities={item.capability for item in CAPABILITY_CASES}, fallback_intents=(), trusted_local={}, ) assert guarded.rejected_requests == () assert len(guarded.plan["requests"]) == 1 guarded_request = guarded.plan["requests"][0] assert guarded_request["capability"] == case.capability assert guarded_request["target_scope"] == case.target assert guarded_request["operation"] == case.operation assert guarded_request["parameters"] == [ {"name": name, "value": value} for name, value in case.parameters.items() ] capability_slug = case.capability.replace(".", "-").replace("_", "-") vehicle_conversation_id = f"conv-matrix-{capability_slug}" client = _ToolCallingCodexClient() assistant = CodexAssistantService(client) tool_results: list[dict[str, object]] = [] raw_results: list[dict[str, object]] = [] if case.expected_effect == "preset_apply": setup_case = _CapabilityCase( "seat.save_posture_preset", "保存当前座椅姿态为矩阵姿态", "passenger", {"preset_name": "矩阵姿态"}, "save", ) setup_result = vehicle_service.preview_native_can( setup_case.message, provider="local", conversation_id=vehicle_conversation_id, execution_mode="dry_run", assistant_plan=setup_case.assistant_plan(), ) assert setup_result["status"] == "succeeded" def invoke_tool(request_token: str, _turn: ActiveCodexTurn) -> None: tool_results.append( assistant.execute_cockpit_request( request_token, proposed_plan, ) ) def execute_control( instruction: str, progress: Callable[[dict[str, object]], None], assistant_plan: Mapping[str, object], ) -> dict[str, object]: assert instruction == case.message result = vehicle_service.preview_native_can( instruction, provider="local", conversation_id=vehicle_conversation_id, execution_mode="dry_run", on_commands_validated=progress, assistant_plan=dict(assistant_plan), ) raw_results.append(result) return result client.on_turn = invoke_tool result, events = _stream_with_catalog( assistant, catalog, case.message, execute_control=execute_control, conversation_id=f"conv-assistant-{capability_slug}", ) assert result["route"] == "agent" assert raw_results[0]["status"] == "succeeded" assert tool_results[0]["verified"] is False assert tool_results[0]["verification_status"] == "simulated" safe_actions = tool_results[0]["actions"] raw_actions = raw_results[0]["intent_ir"]["spec"]["actions"] if case.expected_effect == "action": assert case.capability in {action["capability"] for action in safe_actions} elif case.expected_effect == "preset_save": assert raw_actions == [] assert raw_results[0]["saved_presets"] == [{"name": "矩阵姿态", "target": "passenger"}] else: assert case.expected_effect == "preset_apply" assert raw_results[0]["saved_presets"] == [{"name": "矩阵姿态", "target": "passenger"}] assert {action["capability"] for action in raw_actions} == { "seat.set_longitudinal_position", "seat.set_cushion_tilt", "seat.set_backrest_recline", "seat.set_legrest_height", "seat.set_legrest_extension", } action_event = next(event for event in events if event["type"] == "action.completed") assert action_event["success"] is True def test_every_catalog_mode_executes_through_the_deterministic_assistant_route( vehicle_service: SeatDemoService, catalog: AssistantCapabilityCatalog, ) -> None: snapshot = catalog.snapshot() modes = _repository_catalog_modes(snapshot["modes"]) for index, mode in enumerate(modes): assert isinstance(mode, Mapping) assert mode["assistant_callable"] is True message = f"进入{mode['name']}" client = _ToolCallingCodexClient() assistant = CodexAssistantService(client) raw_results: list[dict[str, object]] = [] tool_results: list[dict[str, object]] = [] def invoke_tool( request_token: str, _turn: ActiveCodexTurn, *, current_results: list[dict[str, object]] = tool_results, current_assistant: CodexAssistantService = assistant, ) -> None: current_results.append(current_assistant.execute_cockpit_request(request_token)) def execute_control( instruction: str, progress: Callable[[dict[str, object]], None], *, current_message: str = message, current_index: int = index, current_results: list[dict[str, object]] = raw_results, ) -> dict[str, object]: assert instruction == current_message result = vehicle_service.preview_native_can( instruction, provider="local", conversation_id=f"conv-mode-matrix-{current_index:02d}", execution_mode="dry_run", on_commands_validated=progress, ) current_results.append(result) return result client.on_turn = invoke_tool result, events = _stream_with_catalog( assistant, catalog, message, execute_control=execute_control, conversation_id=f"conv-assistant-mode-{index:02d}", ) assert raw_results[0]["status"] == "succeeded", mode["id"] assert raw_results[0]["matched_modes"][0]["id"] == mode["id"] assert tool_results == [] assert client.turn_calls == 0 assert result["route"] == "mode_profile_execute" assert result["success"] is True assert events[0]["engine"] == "n7web-mode-profile-runner" def test_every_catalog_mode_rejects_incompatible_explicit_seat_scopes( vehicle_service: SeatDemoService, catalog: AssistantCapabilityCatalog, ) -> None: modes = _repository_catalog_modes(catalog.snapshot()["modes"]) for mode_index, mode in enumerate(modes): assert isinstance(mode, Mapping) for scope_index, scope in enumerate( ("主驾", "驾驶员", "后排", "二排", "前排", "所有座椅", "全车座椅") ): message = f"请让{scope}进入{mode['name']}" result = vehicle_service.preview_native_can( message, provider="local", conversation_id=( f"conv-mode-target-reject-{mode_index:02d}-{scope_index:02d}" ), execution_mode="dry_run", ) assert result["status"] in {"no_action", "needs_confirmation"}, ( mode["id"], scope, ) assert result["intent_ir"]["spec"]["actions"] == [], ( mode["id"], scope, ) assert any( item.get("reason") == "mode_target_scope_mismatch" for item in result["intent_ir"]["spec"].get("ambiguities", []) ), (mode["id"], scope) passenger = vehicle_service.preview_native_can( f"请让副驾进入{mode['name']}", provider="local", conversation_id=f"conv-mode-target-pass-{mode_index:02d}", execution_mode="dry_run", ) assert passenger["status"] == "succeeded", mode["id"] assert passenger["intent_ir"]["spec"]["actions"], mode["id"] def test_preview_and_process_share_mode_target_safety_gate( vehicle_service: SeatDemoService, ) -> None: message = "主驾开启小憩模式" direct = vehicle_service.process( message, provider="local", conversation_id="conv-mode-target-direct", execution_mode="dry_run", ) preview = vehicle_service.preview_native_can( message, provider="local", conversation_id="conv-mode-target-preview", execution_mode="dry_run", ) assert direct["status"] in {"no_action", "needs_confirmation"} assert preview["status"] in {"no_action", "needs_confirmation"} assert direct["intent_ir"]["spec"]["actions"] == [] assert preview["intent_ir"]["spec"]["actions"] == [] assert { item.get("reason") for item in preview["intent_ir"]["spec"].get("ambiguities", []) } == {"mode_target_scope_mismatch"} def test_durable_nfc_peripheral_can_execute_after_bound_card_is_removed( vehicle_service: SeatDemoService, ) -> None: learned = { "id": "star-projector-on", "status": "learned", "enabled": True, "device_id": "star-projector", "device_name": "星空投影仪", "object_names": ["星空投影"], "action": "开启", "action_names": ["打开", "开启"], "card_uid": "A1B2C3D4", "slot": 7, } active_catalog = AssistantCapabilityCatalog( vehicle_service, peripheral_provider=lambda: [learned], nfc_provider=lambda: {"card": {"uid": "a1b2c3d4"}}, ) detached_catalog = AssistantCapabilityCatalog( vehicle_service, peripheral_provider=lambda: [learned], nfc_provider=lambda: {"card": None}, ) active_client = _ToolCallingCodexClient() active_assistant = CodexAssistantService(active_client) executed: list[str] = [] active_client.on_turn = lambda token, _turn: active_assistant.execute_cockpit_request(token) _stream_with_catalog( active_assistant, active_catalog, "打开星空投影仪", execute_control=lambda instruction, _progress: ( executed.append(instruction) or { "status": "succeeded", "execution_mode": "dry_run", "utterance": instruction, "infrared_actions": [ { "capability_id": "star-projector-on", "device_id": "star-projector", "device_name": "星空投影仪", "action": "开启", "slot": 7, "transport": "infrared", } ], } ), conversation_id="conv-active-peripheral-01", ) assert executed == ["打开星空投影仪"] detached_client = _ToolCallingCodexClient() detached_assistant = CodexAssistantService(detached_client) detached_executed: list[str] = [] detached_client.on_turn = ( lambda token, _turn: detached_assistant.execute_cockpit_request(token) ) _stream_with_catalog( detached_assistant, detached_catalog, "打开星空投影仪", execute_control=lambda instruction, _progress: ( detached_executed.append(instruction) or { "status": "succeeded", "execution_mode": "dry_run", "utterance": instruction, "infrared_actions": [ { "capability_id": "star-projector-on", "device_id": "star-projector", "device_name": "星空投影仪", "action": "开启", "slot": 7, "transport": "infrared", } ], } ), conversation_id="conv-detached-peripheral-01", ) assert detached_executed == ["打开星空投影仪"] @pytest.mark.parametrize( "message", [ "整车锁车", "打开车门", "打开壁灯", "打开物理天窗", "打开前大灯", "支持遮阳帘吗", "请把“打开空调”翻译成英文,不要执行", "帮我修改网页里的空调图标", ], ) def test_unsupported_or_non_control_language_cannot_cross_the_execution_boundary( catalog: AssistantCapabilityCatalog, message: str, ) -> None: client = _ToolCallingCodexClient() assistant = CodexAssistantService(client) errors: list[str] = [] def attempt_control(request_token: str, _turn: ActiveCodexTurn) -> None: try: assistant.execute_cockpit_request(request_token) except CodexVehicleToolError as exc: errors.append(str(exc)) client.on_turn = attempt_control _stream_with_catalog( assistant, catalog, message, execute_control=lambda _instruction, _progress: pytest.fail( "unsupported or informational request must not execute" ), conversation_id=f"conv-rejection-{abs(hash(message)):x}", ) assert errors == ["vehicle control was not authorized by the original user request"] def test_capability_fast_route_works_when_codex_is_unavailable( catalog: AssistantCapabilityCatalog, ) -> None: client = _ToolCallingCodexClient(available=False) assistant = CodexAssistantService(client) answer = catalog.fast_answer("车端有哪些可控设备", catalog.snapshot()) assert answer is not None result, events = _stream_with_catalog( assistant, catalog, "车端有哪些可控设备", execute_control=lambda _instruction, _progress: pytest.fail( "capability fast route must never execute controls" ), conversation_id="conv-fast-capability-route", authoritative_answer=answer.answer, ) assert result == {"route": "capability_catalog", "answer": answer.answer} assert client.thread_calls == 0 assert client.turn_calls == 0 assert [event["type"] for event in events] == [ "message.accepted", "work.started", "answer.delta", "work.completed", "answer.completed", ] assert events[0]["engine"] == "n7web-capability-registry" assert events[-1]["route"] == "capability_catalog" def test_hot_comfort_fast_route_reaches_real_climate_service( vehicle_service: SeatDemoService, catalog: AssistantCapabilityCatalog, ) -> None: client = _ToolCallingCodexClient(available=False) assistant = CodexAssistantService(client, fast_implicit_controls=True) raw_results: list[dict[str, object]] = [] def execute_control( instruction: str, progress: Callable[[dict[str, object]], None], ) -> dict[str, object]: result = vehicle_service.preview_native_can( instruction, provider="local", conversation_id="conv-real-hot-comfort", execution_mode="dry_run", on_commands_validated=progress, ) raw_results.append(result) return result result, events = _stream_with_catalog( assistant, catalog, "车上有点热", execute_control=execute_control, conversation_id="conv-fast-hot-real-service", ) assert client.thread_calls == 0 assert client.turn_calls == 0 assert result["route"] == "vehicle_control" assert result["success"] is True assert len(raw_results) == 1 assert raw_results[0]["status"] == "succeeded" actions = raw_results[0]["intent_ir"]["spec"]["actions"] capabilities = [action["capability"] for action in actions] assert capabilities.count("climate.set_power") == 1 assert capabilities.count("climate.set_ac") == 0 assert capabilities.count("climate.set_temperature") == 3 assert capabilities.count("climate.set_fan_level") == 1 assert { action["target"]["zone"] for action in actions if action["capability"] == "climate.set_temperature" } == {"driver", "passenger", "rear"} assert events[-1]["type"] == "answer.completed" assert "23℃" in events[-1]["answer"] def test_turn_bound_capability_query_has_token_and_per_turn_rate_limits( catalog: AssistantCapabilityCatalog, ) -> None: client = _ToolCallingCodexClient() assistant = CodexAssistantService(client) queries: list[str] = [] results: list[dict[str, object]] = [] limit_errors: list[str] = [] control_calls: list[str] = [] def query_capabilities(snapshot: Mapping[str, object], query: str) -> dict[str, object]: queries.append(query) return catalog.query(snapshot, query) def use_tools(request_token: str, _turn: ActiveCodexTurn) -> None: with pytest.raises(ValueError, match="160 characters"): assistant.get_n7web_capabilities(request_token, "x" * 161) for query in ("遮阳帘", "空调", "办公模式", "不存在的设备"): results.append(assistant.get_n7web_capabilities(request_token, query)) try: assistant.get_n7web_capabilities(request_token, "第五次查询") except CodexVehicleToolError as exc: limit_errors.append(str(exc)) assistant.execute_cockpit_request(request_token) client.on_turn = use_tools snapshot = catalog.snapshot() events: list[dict[str, object]] = [] assistant.stream( "打开空调", conversation_id="conv-capability-query-limits", principal="assistant-matrix-user", history=[], emit=lambda event: events.append(event), execute_control=lambda instruction, _progress: ( control_calls.append(instruction) or { "status": "succeeded", "execution_mode": "dry_run", "utterance": instruction, "intent_ir": {"spec": {"actions": []}}, } ), capability_snapshot=snapshot, capability_context=catalog.compact_context(snapshot), control_terms=catalog.control_terms(snapshot), query_capabilities=query_capabilities, ) assert queries == ["遮阳帘", "空调", "办公模式", "不存在的设备"] assert all(result["ok"] is True for result in results) assert all(result["source"] == "n7web_runtime_registry" for result in results) assert results[0]["catalog"]["matched"] is True assert results[-1]["catalog"]["matched"] is False assert limit_errors == ["capability query limit exceeded for this turn"] assert control_calls == ["打开空调"] expired_token = client.request_tokens[0] with pytest.raises(CodexVehicleToolError, match="missing or expired"): assistant.get_n7web_capabilities(expired_token, "空调") with pytest.raises(CodexVehicleToolError, match="missing or expired"): assistant.get_n7web_capabilities("unknown-token", "空调")