| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027 |
- 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"}),
- ("volume to 40.5", {"mode": "absolute", "value": "40.5"}),
- ("volume at 1,000", {"mode": "absolute", "value": "1,000"}),
- ("volume=40.5", {"mode": "absolute", "value": "40.5"}),
- ],
- )
- 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", ["volume to 40", "volume at 40", "volume=40"])
- def test_bare_volume_integer_forms_remain_valid(content):
- volume = RecordingVolumePort()
- registry = build_default_tool_registry(device_volume_port=volume)
- payload = registry.handle(
- _event("bare-integer", "device.volume.adjust", {}),
- EventExecutionContext(history=(ChatMessage(role="user", content=content),)),
- )
- assert payload["port"] == "volume"
- assert volume.calls == [("bare-integer", "absolute", 40, None)]
- @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 == []
|