| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010 |
- import json
- from datetime import datetime, timezone
- import pytest
- import agent_lab.application.events.builtin_plugins as builtin_plugins
- from agent_lab.application.events import (
- EventArgumentResolution,
- EventExecutionContext,
- EventKernel,
- EventRequest,
- EventStatus,
- ResolvedEventArguments,
- ResultPolicy,
- RiskLevel,
- )
- from agent_lab.application.tools import build_default_tool_registry
- from agent_lab.domain.events import ToolCallEvent
- from agent_lab.domain.messages import ChatMessage
- BUILTIN_EVENT_NAMES = [
- "session.terminate",
- "device.volume.adjust",
- "calendar.schedule.create",
- "knowledge.web.search",
- ]
- def _event(event_id: str, name: str, arguments: dict):
- return ToolCallEvent(
- id=event_id,
- name=name,
- arguments=arguments,
- raw_arguments=json.dumps(arguments, ensure_ascii=False),
- )
- def test_default_registry_preserves_legacy_tools_and_adds_builtin_event_plugins():
- registry = build_default_tool_registry()
- assert [item["name"] for item in registry.available_tools()] == [
- "handoff_note",
- "mock_search",
- "mock_ticket",
- *BUILTIN_EVENT_NAMES,
- ]
- @pytest.mark.parametrize(
- (
- "name",
- "result_policy",
- "risk_level",
- "fallback_allowed",
- "idempotency_key_fields",
- "concurrency_class",
- "conflict_keys",
- "timeout_seconds",
- "terminal",
- ),
- [
- (
- "session.terminate",
- ResultPolicy.TERMINATE,
- RiskLevel.HIGH,
- False,
- ("event_id",),
- "session-lifecycle",
- ("session",),
- 5.0,
- True,
- ),
- (
- "device.volume.adjust",
- ResultPolicy.SILENT_SUCCESS,
- RiskLevel.MEDIUM,
- True,
- ("event_id",),
- "device-volume",
- ("device.volume",),
- 5.0,
- False,
- ),
- (
- "calendar.schedule.create",
- ResultPolicy.TEMPLATE_FOLLOW_UP,
- RiskLevel.MEDIUM,
- True,
- ("event_id",),
- "schedule-write",
- ("calendar.schedule",),
- 10.0,
- False,
- ),
- (
- "knowledge.web.search",
- ResultPolicy.LLM_FOLLOW_UP,
- RiskLevel.LOW,
- False,
- (),
- "read-only",
- (),
- 10.0,
- False,
- ),
- ],
- )
- def test_builtin_definition_metadata_is_owned_by_each_flat_plugin(
- name,
- result_policy,
- risk_level,
- fallback_allowed,
- idempotency_key_fields,
- concurrency_class,
- conflict_keys,
- timeout_seconds,
- terminal,
- ):
- definition = build_default_tool_registry().event_registry.definition(name)
- assert definition is not None
- assert definition.result_policy is result_policy
- assert definition.risk_level is risk_level
- assert definition.fallback_allowed is fallback_allowed
- assert definition.idempotency_key_fields == idempotency_key_fields
- assert definition.concurrency_class == concurrency_class
- assert definition.conflict_keys == conflict_keys
- assert definition.timeout_seconds == timeout_seconds
- assert definition.terminal is terminal
- def test_session_terminate_schema_accepts_optional_reason_only():
- registry = build_default_tool_registry().event_registry
- assert registry.iter_validation_errors("session.terminate", {}) == ()
- assert registry.iter_validation_errors(
- "session.terminate", {"reason": "user requested"}
- ) == ()
- assert registry.iter_validation_errors(
- "session.terminate", {"reason": 1}
- )
- assert registry.iter_validation_errors(
- "session.terminate", {"unexpected": True}
- )
- @pytest.mark.parametrize(
- "arguments",
- [
- {"mode": "absolute", "value": 0},
- {"mode": "absolute", "value": 100},
- {"mode": "relative", "delta": -100},
- {"mode": "relative", "delta": 100},
- {"mode": "mute"},
- {"mode": "unmute"},
- ],
- )
- def test_volume_schema_accepts_mode_specific_valid_arguments(arguments):
- registry = build_default_tool_registry().event_registry
- assert registry.iter_validation_errors("device.volume.adjust", arguments) == ()
- @pytest.mark.parametrize(
- "arguments",
- [
- {"mode": "absolute"},
- {"mode": "absolute", "value": -1},
- {"mode": "absolute", "value": 101},
- {"mode": "absolute", "value": 20, "delta": 5},
- {"mode": "relative"},
- {"mode": "relative", "delta": 0},
- {"mode": "relative", "delta": 101},
- {"mode": "relative", "delta": 5, "value": 20},
- {"mode": "mute", "value": 0},
- {"mode": "unmute", "delta": 1},
- ],
- )
- def test_volume_schema_rejects_invalid_or_cross_mode_arguments(arguments):
- registry = build_default_tool_registry().event_registry
- assert registry.iter_validation_errors("device.volume.adjust", arguments)
- @pytest.mark.parametrize(
- ("content", "expected"),
- [
- ("把音量调到 35", {"mode": "absolute", "value": 35}),
- ("set volume to 42", {"mode": "absolute", "value": 42}),
- ("音量增加 15", {"mode": "relative", "delta": 15}),
- ("decrease volume by 20", {"mode": "relative", "delta": -20}),
- ("请静音", {"mode": "mute"}),
- ("please unmute", {"mode": "unmute"}),
- ],
- )
- def test_volume_resolver_recognizes_clear_chinese_and_english(content, expected):
- resolved = _resolve_text_event("device.volume.adjust", content)
- assert resolved == expected
- @pytest.mark.parametrize("content", ["把音量调高", "静音还是取消静音"])
- def test_volume_resolver_marks_incomplete_or_ambiguous_requests_for_fallback(content):
- resolved = _resolve_text_event("device.volume.adjust", content)
- assert isinstance(resolved, EventArgumentResolution)
- assert resolved.complete is False
- def test_schedule_schema_requires_explicit_rfc3339_and_timezone():
- registry = build_default_tool_registry().event_registry
- valid = {
- "title": "Design review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- "recurrence": "FREQ=WEEKLY",
- "reminder_minutes": 15,
- }
- assert registry.iter_validation_errors("calendar.schedule.create", valid) == ()
- assert registry.iter_validation_errors(
- "calendar.schedule.create",
- {**valid, "start_at": "tomorrow at nine"},
- )
- assert registry.iter_validation_errors(
- "calendar.schedule.create",
- {**valid, "timezone": "Shanghai"},
- )
- assert registry.iter_validation_errors(
- "calendar.schedule.create",
- {**valid, "reminder_minutes": -1},
- )
- @pytest.mark.parametrize(
- ("content", "expected"),
- [
- (
- 'Schedule "Design review" at 2026-07-14T09:30:00+08:00 '
- "timezone Asia/Shanghai",
- {
- "title": "Design review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- ),
- (
- "标题:项目评审;开始时间:2026-07-14T09:30:00+08:00;"
- "时区:Asia/Shanghai",
- {
- "title": "项目评审",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- ),
- ],
- )
- def test_schedule_resolver_accepts_only_explicit_datetime_details(content, expected):
- resolved = _resolve_text_event("calendar.schedule.create", content)
- assert resolved == expected
- def test_schedule_resolver_does_not_guess_relative_time():
- resolved = _resolve_text_event(
- "calendar.schedule.create", "Schedule standup tomorrow at nine"
- )
- assert isinstance(resolved, EventArgumentResolution)
- assert resolved.complete is False
- def test_web_search_resolver_uses_latest_relevant_user_request():
- registry = build_default_tool_registry()
- definition = registry.event_registry.definition("knowledge.web.search")
- assert definition is not None
- assert definition.resolver is not None
- resolved = definition.resolver(
- EventRequest(id="search-1", name="knowledge.web.search"),
- EventExecutionContext(
- history=(
- ChatMessage(role="user", content="old query"),
- ChatMessage(role="assistant", content="assistant summary"),
- ChatMessage(role="user", content="latest focused query"),
- ChatMessage(role="assistant", content="I will search"),
- )
- ),
- )
- assert resolved == {"query": "latest focused query"}
- def test_web_search_schema_rejects_empty_query_and_out_of_range_result_limit():
- registry = build_default_tool_registry().event_registry
- assert registry.iter_validation_errors(
- "knowledge.web.search", {"query": "focused", "max_results": 1}
- ) == ()
- assert registry.iter_validation_errors(
- "knowledge.web.search", {"query": "focused", "max_results": 5}
- ) == ()
- assert registry.iter_validation_errors(
- "knowledge.web.search", {"query": ""}
- )
- assert registry.iter_validation_errors(
- "knowledge.web.search", {"query": "focused", "max_results": 0}
- )
- assert registry.iter_validation_errors(
- "knowledge.web.search", {"query": "focused", "max_results": 6}
- )
- def test_catalog_and_direct_provider_modes_share_the_same_builtin_schemas():
- registry = build_default_tool_registry()
- catalog = {item["name"]: item for item in registry.available_tools()}
- direct = {
- item["function"]["name"]: item["function"]
- for item in registry.provider_tool_schemas(BUILTIN_EVENT_NAMES)
- }
- for name in BUILTIN_EVENT_NAMES:
- assert direct[name]["description"] == catalog[name]["description"]
- assert direct[name]["parameters"] == catalog[name]["parameters"]
- class RecordingSessionPort:
- def __init__(self):
- self.calls = []
- def terminate(self, event_id, *, reason=None):
- self.calls.append((event_id, reason))
- return {"port": "session", "event_id": event_id, "reason": reason}
- class RecordingVolumePort:
- def __init__(self):
- self.calls = []
- def adjust(self, event_id, *, mode, value=None, delta=None):
- self.calls.append((event_id, mode, value, delta))
- return {
- "port": "volume",
- "event_id": event_id,
- "mode": mode,
- "value": value,
- "delta": delta,
- }
- class RecordingCalendarPort:
- def __init__(self):
- self.calls = []
- def create(
- self,
- event_id,
- *,
- title,
- start_at,
- timezone,
- recurrence=None,
- reminder_minutes=None,
- ):
- self.calls.append(
- (
- event_id,
- title,
- start_at,
- timezone,
- recurrence,
- reminder_minutes,
- )
- )
- return {"port": "calendar", "event_id": event_id, "title": title}
- class RecordingSearchPort:
- def __init__(self):
- self.calls = []
- def search(self, event_id, *, query, max_results=3):
- self.calls.append((event_id, query, max_results))
- return {"port": "search", "query": query, "max_results": max_results}
- def test_injected_sync_ports_receive_validated_arguments_and_event_ids():
- session = RecordingSessionPort()
- volume = RecordingVolumePort()
- calendar = RecordingCalendarPort()
- search = RecordingSearchPort()
- registry = build_default_tool_registry(
- session_termination_port=session,
- device_volume_port=volume,
- calendar_schedule_port=calendar,
- web_search_port=search,
- )
- assert registry.execute(
- _event("session-1", "session.terminate", {"reason": "done"})
- ) == {"port": "session", "event_id": "session-1", "reason": "done"}
- assert registry.execute(
- _event("volume-1", "device.volume.adjust", {"mode": "absolute", "value": 30})
- )["port"] == "volume"
- assert registry.execute(
- _event(
- "calendar-1",
- "calendar.schedule.create",
- {
- "title": "Review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- "reminder_minutes": 10,
- },
- )
- )["port"] == "calendar"
- assert registry.execute(
- _event(
- "search-1",
- "knowledge.web.search",
- {"query": "agent kernels", "max_results": 2},
- )
- ) == {"port": "search", "query": "agent kernels", "max_results": 2}
- assert session.calls == [("session-1", "done")]
- assert volume.calls == [("volume-1", "absolute", 30, None)]
- assert calendar.calls == [
- (
- "calendar-1",
- "Review",
- "2026-07-14T09:30:00+08:00",
- "Asia/Shanghai",
- None,
- 10,
- )
- ]
- assert search.calls == [("search-1", "agent kernels", 2)]
- @pytest.mark.asyncio
- async def test_injected_async_ports_are_awaited_for_all_builtin_plugins():
- class AsyncSessionPort(RecordingSessionPort):
- async def terminate(self, event_id, *, reason=None):
- return super().terminate(event_id, reason=reason)
- class AsyncVolumePort(RecordingVolumePort):
- async def adjust(self, event_id, *, mode, value=None, delta=None):
- return super().adjust(
- event_id, mode=mode, value=value, delta=delta
- )
- class AsyncCalendarPort(RecordingCalendarPort):
- async def create(self, event_id, **arguments):
- return super().create(event_id, **arguments)
- class AsyncSearchPort(RecordingSearchPort):
- async def search(self, event_id, *, query, max_results=3):
- return super().search(
- event_id, query=query, max_results=max_results
- )
- registry = build_default_tool_registry(
- session_termination_port=AsyncSessionPort(),
- device_volume_port=AsyncVolumePort(),
- calendar_schedule_port=AsyncCalendarPort(),
- web_search_port=AsyncSearchPort(),
- )
- events = [
- _event("session-1", "session.terminate", {}),
- _event(
- "volume-1",
- "device.volume.adjust",
- {"mode": "relative", "delta": -10},
- ),
- _event(
- "calendar-1",
- "calendar.schedule.create",
- {
- "title": "Review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- ),
- _event("search-1", "knowledge.web.search", {"query": "agent kernels"}),
- ]
- payloads = [await registry.execute_async(event) for event in events]
- assert [payload["port"] for payload in payloads] == [
- "session",
- "volume",
- "calendar",
- "search",
- ]
- @pytest.mark.parametrize(
- "arguments",
- [
- {"mode": "absolute", "value": 101},
- {"mode": "relative", "delta": 0},
- {"mode": "mute", "value": 1},
- ],
- )
- def test_invalid_volume_arguments_never_call_the_port(arguments):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- payload = registry.execute(_event("volume-invalid", "device.volume.adjust", arguments))
- assert payload["tool"] == "device.volume.adjust"
- assert "invalid" in payload["error"]
- assert volume.calls == []
- def test_ambiguous_volume_request_without_fallback_never_calls_the_port():
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- payload = registry.handle(
- _event("volume-ambiguous", "device.volume.adjust", {}),
- EventExecutionContext(
- history=(ChatMessage(role="user", content="静音还是取消静音"),)
- ),
- )
- assert payload == {
- "tool": "device.volume.adjust",
- "error": "missing required arguments: mode",
- }
- assert volume.calls == []
- @pytest.mark.parametrize(
- ("port_name", "event"),
- [
- ("session_termination_port", _event("s", "session.terminate", {})),
- (
- "device_volume_port",
- _event("v", "device.volume.adjust", {"mode": "mute"}),
- ),
- (
- "calendar_schedule_port",
- _event(
- "c",
- "calendar.schedule.create",
- {
- "title": "Review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- ),
- ),
- (
- "web_search_port",
- _event("w", "knowledge.web.search", {"query": "agent kernels"}),
- ),
- ],
- )
- def test_port_failures_are_normalized_by_the_generic_kernel(port_name, event):
- class FailingPort:
- def terminate(self, *args, **kwargs):
- raise RuntimeError("port unavailable")
- adjust = terminate
- create = terminate
- search = terminate
- registry = build_default_tool_registry(**{port_name: FailingPort()})
- payload = registry.execute(event)
- assert payload == {
- "tool": event.name,
- "error": "tool handler failed: port unavailable",
- }
- def test_default_stateful_adapters_cache_by_event_id_and_canonical_arguments():
- registry = build_default_tool_registry()
- first_session = registry.execute(
- _event("same-session", "session.terminate", {"reason": "first"})
- )
- replayed_session = registry.execute(
- _event("same-session", "session.terminate", {"reason": "first"})
- )
- changed_session = registry.execute(
- _event("same-session", "session.terminate", {"reason": "second"})
- )
- first_volume = registry.execute(
- _event(
- "same-volume",
- "device.volume.adjust",
- {"mode": " ReLaTiVe ", "delta": 20},
- )
- )
- replayed_volume = registry.execute(
- _event(
- "same-volume",
- "device.volume.adjust",
- {"mode": "relative", "delta": 20},
- )
- )
- changed_volume = registry.execute(
- _event(
- "same-volume",
- "device.volume.adjust",
- {"mode": "relative", "delta": -10},
- )
- )
- first_schedule = registry.execute(
- _event(
- "same-schedule",
- "calendar.schedule.create",
- {
- "title": "First",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- )
- )
- replayed_schedule = registry.execute(
- _event(
- "same-schedule",
- "calendar.schedule.create",
- {
- "title": "First",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- )
- )
- changed_schedule = registry.execute(
- _event(
- "same-schedule",
- "calendar.schedule.create",
- {
- "title": "Second",
- "start_at": "2026-07-15T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- )
- )
- assert replayed_session == first_session
- assert first_session["reason"] == "first"
- assert changed_session["reason"] == "second"
- assert replayed_volume == first_volume
- assert first_volume["delta"] == 20
- assert changed_volume["delta"] == -10
- assert replayed_schedule == first_schedule
- assert first_schedule["schedule"]["title"] == "First"
- assert changed_schedule["schedule"]["title"] == "Second"
- def test_default_web_search_is_deterministic_compact_and_uses_injected_clock():
- registry = build_default_tool_registry(
- clock=lambda: datetime(2030, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
- )
- payload = registry.execute(
- _event(
- "search-1",
- "knowledge.web.search",
- {"query": "agent kernels", "max_results": 2},
- )
- )
- assert payload["tool"] == "knowledge.web.search"
- assert payload["query"] == "agent kernels"
- assert payload["retrieved_at"] == "2030-01-02T03:04:05Z"
- assert len(payload["sources"]) == 2
- assert all(source["url"].startswith("https://example.invalid/") for source in payload["sources"])
- assert all(set(source) == {"title", "url", "snippet"} for source in payload["sources"])
- def test_builtin_port_protocols_and_default_adapter_types_are_public():
- assert builtin_plugins.SessionTerminationPort
- assert builtin_plugins.DeviceVolumePort
- assert builtin_plugins.CalendarSchedulePort
- assert builtin_plugins.WebSearchPort
- assert builtin_plugins.InMemorySessionTerminationAdapter
- assert builtin_plugins.InMemoryDeviceVolumeAdapter
- assert builtin_plugins.InMemoryCalendarScheduleAdapter
- assert builtin_plugins.InMemoryWebSearchAdapter
- def _resolve_text_event(name: str, content: str):
- registry = build_default_tool_registry()
- definition = registry.event_registry.definition(name)
- assert definition is not None
- assert definition.resolver is not None
- return definition.resolver(
- EventRequest(id="event-1", name=name),
- EventExecutionContext(history=(ChatMessage(role="user", content=content),)),
- )
- def test_volume_structured_arguments_are_canonical_in_text_and_provider_modes():
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- arguments = {"mode": " AbSoLuTe ", "value": 40}
- text_payload = registry.handle(
- _event("text-volume", "device.volume.adjust", arguments)
- )
- provider_payload = registry.execute(
- _event("provider-volume", "device.volume.adjust", arguments)
- )
- assert text_payload["port"] == "volume"
- assert provider_payload["port"] == "volume"
- assert volume.calls == [
- ("text-volume", "absolute", 40, None),
- ("provider-volume", "absolute", 40, None),
- ]
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "content",
- [
- "不静音",
- "不用静音",
- "无需静音",
- "no mute",
- "don't mute",
- "do not mute",
- ],
- )
- async def test_negated_volume_requests_are_substantive_invalid_without_fallback(
- content,
- ):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- fallback_calls = 0
- async def fallback(*args):
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="device.volume.adjust",
- arguments={"mode": "mute"},
- raw_arguments='{"mode":"mute"}',
- )
- result = await EventKernel(
- registry.event_registry,
- argument_fallback=fallback,
- ).execute(
- registry.event_request(_event("negated-volume", "device.volume.adjust", {})),
- enabled_names=["device.volume.adjust"],
- context=EventExecutionContext(
- history=(ChatMessage(role="user", content=content),)
- ),
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.arguments == {"mode": "unsupported"}
- assert result.used_fallback is False
- assert fallback_calls == 0
- assert volume.calls == []
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("content", "expected_arguments"),
- [
- ("set volume to 40.5", {"mode": "absolute", "value": "40.5"}),
- ("increase volume by 10.5", {"mode": "relative", "delta": "10.5"}),
- ("set volume to 1,000", {"mode": "absolute", "value": "1,000"}),
- ("音量增加 1,000", {"mode": "relative", "delta": "1,000"}),
- ],
- )
- async def test_unsupported_volume_numeric_tokens_are_not_truncated_or_fallback(
- content,
- expected_arguments,
- ):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- fallback_calls = 0
- async def fallback(*args):
- nonlocal fallback_calls
- fallback_calls += 1
- raise AssertionError("unsupported numeric tokens must not fallback")
- result = await EventKernel(
- registry.event_registry,
- argument_fallback=fallback,
- ).execute(
- registry.event_request(_event("invalid-number", "device.volume.adjust", {})),
- enabled_names=["device.volume.adjust"],
- context=EventExecutionContext(
- history=(ChatMessage(role="user", content=content),)
- ),
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.arguments == expected_arguments
- assert result.used_fallback is False
- assert fallback_calls == 0
- assert volume.calls == []
- @pytest.mark.parametrize("content", ["set volume to 1000", "音量增加 1000"])
- def test_volume_resolver_captures_full_oversized_number_and_rejects_it(content):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- payload = registry.handle(
- _event("oversized-volume", "device.volume.adjust", {}),
- EventExecutionContext(history=(ChatMessage(role="user", content=content),)),
- )
- assert payload["tool"] == "device.volume.adjust"
- assert "invalid" in payload["error"]
- assert volume.calls == []
- def test_provider_search_query_is_trimmed_before_port_execution():
- search = RecordingSearchPort()
- registry = build_default_tool_registry(web_search_port=search)
- payload = registry.execute(
- _event(
- "provider-search",
- "knowledge.web.search",
- {"query": " focused query ", "max_results": 2},
- )
- )
- assert payload == {"port": "search", "query": "focused query", "max_results": 2}
- assert search.calls == [("provider-search", "focused query", 2)]
- def test_whitespace_only_provider_search_query_never_calls_the_port():
- search = RecordingSearchPort()
- registry = build_default_tool_registry(web_search_port=search)
- payload = registry.execute(
- _event("blank-search", "knowledge.web.search", {"query": " "})
- )
- assert payload["tool"] == "knowledge.web.search"
- assert "invalid" in payload["error"]
- assert search.calls == []
- def test_schedule_catalog_uses_standard_datetime_and_iana_timezone_formats():
- registry = build_default_tool_registry()
- schema = registry.event_registry.definition(
- "calendar.schedule.create"
- ).parameters
- assert schema["properties"]["start_at"]["format"] == "date-time"
- assert schema["properties"]["timezone"]["format"] == "iana-timezone"
- def test_schedule_standard_datetime_accepts_lowercase_tz_and_rejects_bad_date():
- registry = build_default_tool_registry().event_registry
- base = {
- "title": "Review",
- "timezone": "Asia/Shanghai",
- }
- assert registry.iter_validation_errors(
- "calendar.schedule.create",
- {**base, "start_at": "2026-07-14t09:30:00z"},
- ) == ()
- assert registry.iter_validation_errors(
- "calendar.schedule.create",
- {**base, "start_at": "2026-02-31T09:30:00Z"},
- )
- @pytest.mark.parametrize(
- "arguments",
- [
- {
- "title": "Impossible date",
- "start_at": "2026-02-30T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- {
- "title": "Unknown zone",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Mars/Olympus",
- },
- {
- "title": "Unsafe zone",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "../etc/passwd",
- },
- {
- "title": " ",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- },
- ],
- )
- def test_invalid_schedule_boundaries_never_call_the_port(arguments):
- calendar = RecordingCalendarPort()
- registry = build_default_tool_registry(calendar_schedule_port=calendar)
- payload = registry.execute(
- _event("invalid-schedule", "calendar.schedule.create", arguments)
- )
- assert payload["tool"] == "calendar.schedule.create"
- assert "invalid" in payload["error"]
- assert calendar.calls == []
- def test_integral_float_numeric_arguments_reach_ports_as_ints():
- volume = RecordingVolumePort()
- calendar = RecordingCalendarPort()
- search = RecordingSearchPort()
- registry = build_default_tool_registry(
- device_volume_port=volume,
- calendar_schedule_port=calendar,
- web_search_port=search,
- )
- registry.execute(
- _event(
- "absolute-float",
- "device.volume.adjust",
- {"mode": "absolute", "value": 40.0},
- )
- )
- registry.execute(
- _event(
- "relative-float",
- "device.volume.adjust",
- {"mode": "relative", "delta": -10.0},
- )
- )
- registry.execute(
- _event(
- "schedule-float",
- "calendar.schedule.create",
- {
- "title": "Review",
- "start_at": "2026-07-14T09:30:00+08:00",
- "timezone": "Asia/Shanghai",
- "reminder_minutes": 15.0,
- },
- )
- )
- registry.execute(
- _event(
- "search-float",
- "knowledge.web.search",
- {"query": "focused", "max_results": 2.0},
- )
- )
- assert volume.calls == [
- ("absolute-float", "absolute", 40, None),
- ("relative-float", "relative", None, -10),
- ]
- assert type(volume.calls[0][2]) is int
- assert type(volume.calls[1][3]) is int
- assert calendar.calls[0][-1] == 15
- assert type(calendar.calls[0][-1]) is int
- assert search.calls == [("search-float", "focused", 2)]
- assert type(search.calls[0][-1]) is int
- def test_integral_float_and_int_share_the_same_volume_cache_key():
- adapter = builtin_plugins.InMemoryDeviceVolumeAdapter()
- registry = build_default_tool_registry(device_volume_port=adapter)
- first = registry.execute(
- _event(
- "same-number",
- "device.volume.adjust",
- {"mode": "absolute", "value": 40},
- )
- )
- replay = registry.execute(
- _event(
- "same-number",
- "device.volume.adjust",
- {"mode": "absolute", "value": 40.0},
- )
- )
- assert replay == first
- assert len(adapter._results) == 1
- @pytest.mark.parametrize(
- "arguments",
- [
- {"mode": "absolute", "value": True},
- {"mode": "relative", "delta": False},
- ],
- )
- def test_volume_normalizer_does_not_treat_bool_as_integral_number(arguments):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- payload = registry.execute(_event("bool-volume", "device.volume.adjust", arguments))
- assert payload["tool"] == "device.volume.adjust"
- assert "invalid" in payload["error"]
- assert volume.calls == []
|