| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312 |
- from __future__ import annotations
- import gc
- import json
- import warnings
- from typing import Any
- import pytest
- from agent_lab.application.events import (
- ConfirmationPolicy,
- EventDefinition,
- EventExecutionContext,
- EventKernel,
- EventRegistry,
- EventRequest,
- ResolvedEventArguments,
- EventSource,
- EventStatus,
- ResultPolicy,
- RiskLevel,
- )
- from agent_lab.application.events.models import EventArgumentResolution
- from agent_lab.application.tools import (
- ToolDefinition,
- ToolExecutionContext,
- ToolRegistry,
- )
- from agent_lab.domain.events import ToolCallEvent
- from agent_lab.domain.messages import ChatMessage
- def _definition(
- name: str = "example.lookup",
- **overrides: Any,
- ) -> EventDefinition:
- values: dict[str, Any] = {
- "name": name,
- "description": "Look up an example value.",
- "parameters": {
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- },
- "handler": lambda request: {
- "event": request.name,
- "query": request.arguments["query"],
- },
- }
- values.update(overrides)
- return EventDefinition(**values)
- def test_registry_registers_flat_definitions_and_filters_enabled_catalog():
- registry = EventRegistry(
- [_definition("example.lookup"), _definition("device.inspect")]
- )
- assert [item["name"] for item in registry.catalog()] == [
- "example.lookup",
- "device.inspect",
- ]
- assert registry.catalog(["device.inspect"]) == [
- {
- "name": "device.inspect",
- "description": "Look up an example value.",
- "parameters": {
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- },
- }
- ]
- assert registry.tool_schema("example.lookup")["function"]["name"] == (
- "example.lookup"
- )
- assert registry.tool_schema("missing") is None
- def test_registry_rejects_duplicate_definition_names():
- with pytest.raises(ValueError, match="duplicate event definition: example.lookup"):
- EventRegistry([_definition(), _definition()])
- def test_registry_rejects_invalid_draft_2020_12_schema():
- definition = _definition(parameters={"type": 42})
- with pytest.raises(ValueError, match="invalid event schema for example.lookup"):
- EventRegistry([definition])
- def test_registry_does_not_expose_mutable_validator_instances():
- registry = EventRegistry([_definition()])
- assert not hasattr(registry, "validator")
- errors = list(
- registry.iter_validation_errors(
- "example.lookup",
- {"query": 42},
- )
- )
- assert errors
- errors[0].schema["type"] = "integer"
- assert list(
- registry.iter_validation_errors(
- "example.lookup",
- {"query": 42},
- )
- )
- def test_tool_registry_public_api_remains_compatible():
- registry = ToolRegistry(
- [
- ToolDefinition(
- name="compat.lookup",
- description="Look up compatibility data.",
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- },
- handler=lambda event: {
- "tool": event.name,
- "query": event.arguments["query"],
- },
- argument_resolver=lambda event, context: {
- "query": context.history[-1].content
- },
- )
- ]
- )
- event = ToolCallEvent(
- id="call-1",
- name="compat.lookup",
- arguments={"query": "provider"},
- raw_arguments='{"query":"provider"}',
- )
- assert registry.available_tools() == [
- {
- "name": "compat.lookup",
- "description": "Look up compatibility data.",
- "parameters": {
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- },
- }
- ]
- assert "- compat.lookup: Look up compatibility data." in (
- registry.chat_event_system_message(["compat.lookup"])
- )
- assert registry.tool_schema("compat.lookup")["function"]["name"] == (
- "compat.lookup"
- )
- assert registry.handle(
- event,
- ToolExecutionContext(history=[ChatMessage(role="user", content="history")]),
- ) == {"tool": "compat.lookup", "query": "history"}
- assert registry.execute(event) == {"tool": "compat.lookup", "query": "provider"}
- @pytest.mark.asyncio
- async def test_kernel_executes_complete_deterministic_arguments_without_fallback():
- fallback_calls: list[str] = []
- async def fallback(*args: Any) -> dict[str, Any]:
- fallback_calls.append("called")
- return {"query": "fallback"}
- registry = EventRegistry(
- [_definition(resolver=lambda request, context: {"query": "deterministic"})]
- )
- result = await EventKernel(registry, argument_fallback=fallback).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.arguments == {"query": "deterministic"}
- assert result.payload == {"event": "example.lookup", "query": "deterministic"}
- assert result.used_fallback is False
- assert fallback_calls == []
- @pytest.mark.asyncio
- async def test_kernel_calls_fallback_once_when_required_arguments_are_incomplete():
- fallback_calls: list[dict[str, Any]] = []
- async def fallback(
- definition: EventDefinition,
- request: EventRequest,
- context: EventExecutionContext,
- ) -> ResolvedEventArguments:
- fallback_calls.append(dict(request.arguments))
- return ResolvedEventArguments(
- event_name=definition.name,
- arguments={"query": "resolved once"},
- raw_arguments='{"query":"resolved once"}',
- )
- registry = EventRegistry([_definition(resolver=lambda request, context: {})])
- result = await EventKernel(registry, argument_fallback=fallback).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.arguments == {"query": "resolved once"}
- assert result.used_fallback is True
- assert fallback_calls == [{}]
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "arguments",
- [
- {"query": 42},
- {"query": "unsupported"},
- {"query": "valid", "unexpected": True},
- ],
- )
- async def test_kernel_does_not_fallback_for_complete_invalid_arguments(
- arguments: dict[str, Any],
- ):
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "valid"},
- raw_arguments='{"query":"valid"}',
- )
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string", "enum": ["valid"]}},
- "required": ["query"],
- "additionalProperties": False,
- },
- resolver=lambda request, context: arguments,
- )
- result = await EventKernel(
- EventRegistry([definition]), argument_fallback=fallback
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.used_fallback is False
- assert fallback_calls == 0
- @pytest.mark.asyncio
- @pytest.mark.parametrize("composition", ["anyOf", "oneOf"])
- async def test_kernel_falls_back_for_required_missing_inside_composed_schema(
- composition: str,
- ):
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"choice": {"mode": "alpha"}},
- raw_arguments='{"choice":{"mode":"alpha"}}',
- )
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {
- "choice": {
- composition: [
- {
- "type": "object",
- "properties": {"mode": {"const": "alpha"}},
- "required": ["mode"],
- },
- {
- "type": "object",
- "properties": {"mode": {"const": "beta"}},
- "required": ["mode"],
- },
- ]
- }
- },
- "required": ["choice"],
- },
- resolver=lambda request, context: {"choice": {}},
- handler=lambda request: {"ok": True},
- )
- result = await EventKernel(
- EventRegistry([definition]), argument_fallback=fallback
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.used_fallback is True
- assert fallback_calls == 1
- @pytest.mark.asyncio
- @pytest.mark.parametrize("composition", ["anyOf", "oneOf"])
- async def test_kernel_does_not_fallback_for_complete_invalid_composed_schema(
- composition: str,
- ):
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- raise AssertionError("fallback should not run")
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {
- "choice": {
- composition: [
- {
- "type": "object",
- "properties": {"mode": {"const": "alpha"}},
- "required": ["mode"],
- },
- {
- "type": "object",
- "properties": {"mode": {"const": "beta"}},
- "required": ["mode"],
- },
- ]
- }
- },
- "required": ["choice"],
- },
- resolver=lambda request, context: {"choice": {"mode": "other"}},
- handler=lambda request: {"ok": True},
- )
- result = await EventKernel(
- EventRegistry([definition]), argument_fallback=fallback
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.used_fallback is False
- assert fallback_calls == 0
- @pytest.mark.asyncio
- async def test_kernel_does_not_fallback_when_definition_disallows_it():
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "not allowed"},
- raw_arguments='{"query":"not allowed"}',
- )
- registry = EventRegistry(
- [
- _definition(
- resolver=lambda request, context: {},
- fallback_allowed=False,
- )
- ]
- )
- result = await EventKernel(registry, argument_fallback=fallback).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.error == "missing required arguments: query"
- assert result.used_fallback is False
- assert fallback_calls == 0
- @pytest.mark.asyncio
- async def test_structured_resolution_can_mark_optional_arguments_incomplete():
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "resolved"},
- raw_arguments='{"query":"resolved"}',
- )
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "additionalProperties": False,
- },
- resolver=lambda request, context: EventArgumentResolution(
- arguments={},
- complete=False,
- ),
- )
- result = await EventKernel(
- EventRegistry([definition]), argument_fallback=fallback
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.arguments == {"query": "resolved"}
- assert result.used_fallback is True
- assert fallback_calls == 1
- @pytest.mark.asyncio
- async def test_structured_incomplete_optional_arguments_fail_without_fallback():
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- },
- resolver=lambda request, context: EventArgumentResolution(
- arguments={},
- complete=False,
- ),
- handler=lambda request: {"ok": True},
- )
- result = await EventKernel(EventRegistry([definition])).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.error == "event arguments incomplete"
- @pytest.mark.asyncio
- async def test_plain_dict_resolution_remains_complete_for_optional_schema():
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- raise AssertionError("fallback should not run")
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- },
- resolver=lambda request, context: {},
- handler=lambda request: {"ok": True},
- )
- result = await EventKernel(
- EventRegistry([definition]), argument_fallback=fallback
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert fallback_calls == 0
- def test_sync_kernel_rejects_structured_incomplete_optional_arguments():
- handler_calls = 0
- def handler(request: EventRequest) -> dict[str, Any]:
- nonlocal handler_calls
- handler_calls += 1
- return {"ok": True}
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- },
- resolver=lambda request, context: EventArgumentResolution(
- arguments={},
- complete=False,
- ),
- handler=handler,
- )
- result = EventKernel(EventRegistry([definition])).execute_sync(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.error == "event arguments incomplete"
- assert handler_calls == 0
- @pytest.mark.parametrize(
- "resolver",
- [
- lambda request, context: EventArgumentResolution(
- arguments={},
- complete=True,
- ),
- lambda request, context: {},
- ],
- )
- def test_sync_kernel_preserves_complete_compatible_resolvers(resolver: Any):
- handler_calls = 0
- def handler(request: EventRequest) -> dict[str, Any]:
- nonlocal handler_calls
- handler_calls += 1
- return {"ok": True}
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- },
- resolver=resolver,
- handler=handler,
- )
- result = EventKernel(EventRegistry([definition])).execute_sync(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.payload == {"ok": True}
- assert handler_calls == 1
- @pytest.mark.asyncio
- async def test_provider_resolved_arguments_are_not_rewritten_or_fallen_back():
- resolver_calls = 0
- fallback_calls = 0
- def resolver(*args: Any) -> dict[str, Any]:
- nonlocal resolver_calls
- resolver_calls += 1
- return {"query": "rewritten"}
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "fallback"},
- raw_arguments='{"query":"fallback"}',
- )
- registry = EventRegistry([_definition(resolver=resolver)])
- result = await EventKernel(registry, argument_fallback=fallback).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "provider value"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.arguments == {"query": "provider value"}
- assert resolver_calls == 0
- assert fallback_calls == 0
- @pytest.mark.asyncio
- async def test_provider_resolved_missing_arguments_return_invalid_without_fallback():
- fallback_calls = 0
- async def fallback(*args: Any) -> ResolvedEventArguments:
- nonlocal fallback_calls
- fallback_calls += 1
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "fallback"},
- raw_arguments='{"query":"fallback"}',
- )
- result = await EventKernel(
- EventRegistry([_definition()]), argument_fallback=fallback
- ).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.error == "missing required arguments: query"
- assert fallback_calls == 0
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("event_request", "enabled_names", "expected_status", "expected_error"),
- [
- (
- EventRequest(id="event-1", name="missing"),
- ["missing"],
- EventStatus.UNKNOWN,
- "unknown event",
- ),
- (
- EventRequest(id="event-1", name="example.lookup"),
- [],
- EventStatus.DISABLED,
- "event disabled",
- ),
- (
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": 42},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- ["example.lookup"],
- EventStatus.INVALID_ARGUMENTS,
- "invalid argument type for query: expected string",
- ),
- ],
- )
- async def test_kernel_normalizes_lookup_and_validation_failures(
- event_request: EventRequest,
- enabled_names: list[str],
- expected_status: EventStatus,
- expected_error: str,
- ):
- result = await EventKernel(EventRegistry([_definition()])).execute(
- event_request,
- enabled_names=enabled_names,
- )
- assert result.status is expected_status
- assert result.error == expected_error
- @pytest.mark.asyncio
- async def test_kernel_normalizes_handler_exceptions():
- def fail(request: EventRequest) -> dict[str, Any]:
- raise RuntimeError("boom")
- result = await EventKernel(
- EventRegistry([_definition(handler=fail)])
- ).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "value"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.HANDLER_ERROR
- assert result.error == "event handler failed: boom"
- @pytest.mark.asyncio
- async def test_kernel_validates_complete_draft_2020_12_schema():
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {
- "mode": {"enum": ["quick", "deep"]},
- "target": {"type": ["string", "null"]},
- "filters": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {"score": {"type": "number", "minimum": 0}},
- "required": ["score"],
- "additionalProperties": False,
- },
- },
- },
- "required": ["mode", "target", "filters"],
- "additionalProperties": False,
- },
- handler=lambda request: {"event": request.name},
- )
- kernel = EventKernel(EventRegistry([definition]))
- valid = await kernel.execute(
- EventRequest(
- id="valid",
- name=definition.name,
- arguments={
- "mode": "deep",
- "target": None,
- "filters": [{"score": 0.5}],
- },
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=[definition.name],
- )
- invalid = await kernel.execute(
- EventRequest(
- id="invalid",
- name=definition.name,
- arguments={
- "mode": "other",
- "target": 7,
- "filters": [{"score": -1, "extra": True}],
- "unexpected": True,
- },
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=[definition.name],
- )
- assert valid.status is EventStatus.SUCCESS
- assert invalid.status is EventStatus.INVALID_ARGUMENTS
- assert invalid.error.startswith("invalid event arguments:")
- @pytest.mark.asyncio
- async def test_kernel_normalizes_validator_runtime_exceptions():
- definition = _definition(parameters={"$ref": "urn:agent-lab:missing-schema"})
- result = await EventKernel(EventRegistry([definition])).execute(
- EventRequest(
- id="event-1",
- name=definition.name,
- arguments={},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=[definition.name],
- )
- assert result.status is EventStatus.DEFINITION_ERROR
- assert result.error.startswith("event argument validation failed:")
- @pytest.mark.asyncio
- async def test_tool_registry_maps_definition_errors_to_tool_compatibility_payload():
- registry = ToolRegistry(
- [
- ToolDefinition(
- name="broken.lookup",
- description="Broken lookup.",
- parameters={"$ref": "urn:agent-lab:missing-schema"},
- handler=lambda event: {"tool": event.name},
- )
- ]
- )
- payload = await registry.execute_async(
- ToolCallEvent(
- id="call-1",
- name="broken.lookup",
- arguments={},
- raw_arguments="{}",
- )
- )
- assert payload["tool"] == "broken.lookup"
- assert payload["error"].startswith("tool definition validation failed:")
- @pytest.mark.asyncio
- @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
- async def test_kernel_normalizes_resolution_boundary_exceptions(boundary: str):
- def resolver(request: EventRequest, context: EventExecutionContext) -> dict[str, Any]:
- if boundary == "resolver":
- raise RuntimeError("resolver boom")
- return {}
- async def fallback(*args: Any) -> ResolvedEventArguments:
- raise RuntimeError("fallback boom")
- result = await EventKernel(
- EventRegistry([_definition(resolver=resolver)]),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- assert result.error == f"event argument {boundary} failed: {boundary} boom"
- @pytest.mark.asyncio
- @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
- async def test_kernel_normalizes_invalid_resolution_payloads(boundary: str):
- resolver = (
- (lambda request, context: None)
- if boundary == "resolver"
- else (lambda request, context: {})
- )
- async def fallback(*args: Any) -> Any:
- return {"query": "legacy bare mapping"}
- result = await EventKernel(
- EventRegistry([_definition(resolver=resolver)]),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- assert result.error == f"event argument {boundary} returned invalid payload"
- @pytest.mark.asyncio
- async def test_kernel_normalizes_non_json_resolver_arguments():
- result = await EventKernel(
- EventRegistry(
- [
- _definition(
- resolver=lambda request, context: {"query": object()}
- )
- ]
- )
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- assert result.error.startswith("event argument resolver failed to serialize:")
- @pytest.mark.asyncio
- async def test_kernel_normalizes_non_object_fallback_arguments():
- async def fallback(*args: Any) -> ResolvedEventArguments:
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments=["not", "an", "object"], # type: ignore[arg-type]
- raw_arguments='["not","an","object"]',
- )
- result = await EventKernel(
- EventRegistry([_definition(resolver=lambda request, context: {})]),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- assert result.error == "event argument fallback returned invalid payload"
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("resolved", "expected_error"),
- [
- (
- ResolvedEventArguments(
- event_name="another.event",
- arguments={"query": "value"},
- raw_arguments='{"query":"value"}',
- ),
- "fallback returned tool another.event for example.lookup",
- ),
- (
- ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "value"},
- raw_arguments="not-json",
- ),
- "fallback raw arguments are not valid JSON",
- ),
- (
- ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "parsed"},
- raw_arguments='{"query":"raw"}',
- ),
- "fallback raw arguments do not match parsed arguments",
- ),
- ],
- )
- async def test_kernel_rejects_inconsistent_structured_fallback(
- resolved: ResolvedEventArguments,
- expected_error: str,
- ):
- async def fallback(*args: Any) -> ResolvedEventArguments:
- return resolved
- result = await EventKernel(
- EventRegistry([_definition(resolver=lambda request, context: {})]),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- assert result.error == expected_error
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("arguments", "raw_arguments"),
- [
- ({"query": 1}, '{"query":true}'),
- ({"query": 1.0}, '{"query":1}'),
- ({"query": {"nested": [1]}}, '{"query":{"nested":[true]}}'),
- ({"query": float("nan")}, '{"query":NaN}'),
- ({"query": float("inf")}, '{"query":Infinity}'),
- ],
- )
- async def test_kernel_rejects_noncanonical_fallback_json(
- arguments: dict[str, Any],
- raw_arguments: str,
- ):
- async def fallback(*args: Any) -> ResolvedEventArguments:
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments=arguments,
- raw_arguments=raw_arguments,
- )
- result = await EventKernel(
- EventRegistry([_definition(resolver=lambda request, context: {})]),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.RESOLUTION_ERROR
- @pytest.mark.asyncio
- async def test_registry_schema_is_isolated_from_caller_mutation():
- parameters = {
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- "additionalProperties": False,
- }
- registry = EventRegistry([_definition(parameters=parameters)])
- parameters["properties"]["query"]["type"] = "integer"
- parameters["required"].clear()
- parameters["additionalProperties"] = True
- assert registry.catalog()[0]["parameters"] == {
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- "additionalProperties": False,
- }
- registered = registry.definition("example.lookup")
- assert registered is not None
- with pytest.raises(TypeError):
- registered.parameters["additionalProperties"] = True
- with pytest.raises(TypeError):
- registered.parameters["properties"]["query"]["type"] = "integer"
- with pytest.raises(AttributeError):
- registered.parameters["required"].append("unexpected")
- result = await EventKernel(registry).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": 42, "unexpected": True},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- @pytest.mark.asyncio
- async def test_kernel_passes_consistent_fallback_arguments_and_raw_json_to_handler():
- captured: list[EventRequest] = []
- async def fallback(*args: Any) -> ResolvedEventArguments:
- return ResolvedEventArguments(
- event_name="example.lookup",
- arguments={"query": "resolved"},
- raw_arguments='{"query":"resolved"}',
- )
- result = await EventKernel(
- EventRegistry(
- [
- _definition(
- resolver=lambda request, context: {},
- handler=lambda request: captured.append(request) or {"ok": True},
- )
- ]
- ),
- argument_fallback=fallback,
- ).execute(
- EventRequest(id="event-1", name="example.lookup"),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert captured[0].arguments == json.loads(captured[0].raw_arguments)
- assert result.raw_arguments == captured[0].raw_arguments
- @pytest.mark.asyncio
- @pytest.mark.parametrize("execution", ["async", "sync"])
- async def test_kernel_isolates_nested_handler_mutation_from_audit_values(
- execution: str,
- ):
- caller_arguments = {"nested": {"items": ["original"]}}
- handler_arguments: list[dict[str, Any]] = []
- def handler(request: EventRequest) -> dict[str, Any]:
- handler_arguments.append(request.arguments)
- request.arguments["nested"]["items"].append("handler")
- return {"nested": request.arguments["nested"]}
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {
- "nested": {
- "type": "object",
- "properties": {
- "items": {"type": "array", "items": {"type": "string"}}
- },
- "required": ["items"],
- }
- },
- "required": ["nested"],
- },
- handler=handler,
- )
- request = EventRequest(
- id="event-1",
- name="example.lookup",
- arguments=caller_arguments,
- raw_arguments='{"nested":{"items":["original"]}}',
- source=EventSource.PROVIDER_RESOLVED,
- )
- kernel = EventKernel(EventRegistry([definition]))
- result = (
- await kernel.execute(request, enabled_names=["example.lookup"])
- if execution == "async"
- else kernel.execute_sync(request, enabled_names=["example.lookup"])
- )
- assert result.status is EventStatus.SUCCESS
- assert caller_arguments == {"nested": {"items": ["original"]}}
- assert request.arguments == {"nested": {"items": ["original"]}}
- assert result.arguments == {"nested": {"items": ["original"]}}
- assert json.loads(result.raw_arguments) == result.arguments
- assert result.payload == {"nested": {"items": ["original", "handler"]}}
- handler_arguments[0]["nested"]["items"].append("later")
- assert result.payload == {"nested": {"items": ["original", "handler"]}}
- result.payload["nested"]["items"].append("result")
- assert result.arguments == {"nested": {"items": ["original"]}}
- assert caller_arguments == {"nested": {"items": ["original"]}}
- @pytest.mark.asyncio
- @pytest.mark.parametrize("payload", [None, "text", 1, ["item"]])
- async def test_kernel_normalizes_invalid_handler_payloads(payload: Any):
- result = await EventKernel(
- EventRegistry([_definition(handler=lambda request: payload)])
- ).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "value"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.HANDLER_ERROR
- assert result.error == "event handler returned non-object payload"
- @pytest.mark.asyncio
- @pytest.mark.parametrize("execution", ["async", "sync"])
- @pytest.mark.parametrize(
- "invalid_value",
- [object(), {"set-item"}, float("nan"), float("inf")],
- )
- async def test_kernel_rejects_non_json_handler_dictionary_payloads(
- execution: str,
- invalid_value: Any,
- ):
- definition = _definition(
- handler=lambda request: {"invalid": invalid_value},
- )
- request = EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "value"},
- raw_arguments='{"query":"value"}',
- source=EventSource.PROVIDER_RESOLVED,
- )
- kernel = EventKernel(EventRegistry([definition]))
- result = (
- await kernel.execute(request, enabled_names=["example.lookup"])
- if execution == "async"
- else kernel.execute_sync(request, enabled_names=["example.lookup"])
- )
- assert result.status is EventStatus.HANDLER_ERROR
- assert result.error == "event handler returned non-JSON payload"
- @pytest.mark.asyncio
- async def test_async_kernel_supports_async_handler():
- async def handler(request: EventRequest) -> dict[str, Any]:
- return {"query": request.arguments["query"]}
- result = await EventKernel(
- EventRegistry([_definition(handler=handler)])
- ).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "async"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.payload == {"query": "async"}
- @pytest.mark.asyncio
- async def test_tool_registry_async_entry_points_support_async_handler():
- async def handler(event: ToolCallEvent) -> dict[str, Any]:
- return {"tool": event.name, "query": event.arguments["query"]}
- registry = ToolRegistry(
- [
- ToolDefinition(
- name="async.lookup",
- description="Async lookup.",
- parameters={
- "type": "object",
- "properties": {"query": {"type": "string"}},
- "required": ["query"],
- },
- handler=handler,
- )
- ]
- )
- event = ToolCallEvent(
- id="call-1",
- name="async.lookup",
- arguments={"query": "value"},
- raw_arguments='{"query":"value"}',
- )
- assert await registry.handle_async(event) == {
- "tool": "async.lookup",
- "query": "value",
- }
- assert await registry.execute_async(event) == {
- "tool": "async.lookup",
- "query": "value",
- }
- def test_tool_registry_sync_facade_rejects_async_handler_without_runtime_warning():
- called = False
- async def handler(event: ToolCallEvent) -> dict[str, Any]:
- nonlocal called
- called = True
- return {"tool": event.name}
- registry = ToolRegistry(
- [
- ToolDefinition(
- name="async.lookup",
- description="Async lookup.",
- parameters={"type": "object"},
- handler=handler,
- )
- ]
- )
- event = ToolCallEvent(
- id="call-1",
- name="async.lookup",
- arguments={},
- raw_arguments="{}",
- )
- with warnings.catch_warnings(record=True) as captured:
- warnings.simplefilter("always")
- payload = registry.execute(event)
- gc.collect()
- assert payload == {
- "tool": "async.lookup",
- "error": "tool handler failed: async event handlers require execute_async",
- }
- assert called is False
- assert not [warning for warning in captured if issubclass(warning.category, RuntimeWarning)]
- @pytest.mark.asyncio
- async def test_kernel_rejects_boolean_for_json_number_arguments():
- definition = _definition(
- parameters={
- "type": "object",
- "properties": {"query": {"type": "number"}},
- "required": ["query"],
- }
- )
- result = await EventKernel(EventRegistry([definition])).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": True},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- assert result.status is EventStatus.INVALID_ARGUMENTS
- assert result.error == "invalid argument type for query: expected number"
- @pytest.mark.asyncio
- async def test_definition_metadata_survives_registration_and_result_creation():
- definition = _definition(
- result_policy=ResultPolicy.TEMPLATE_FOLLOW_UP,
- confirmation_policy=ConfirmationPolicy.REQUIRED,
- risk_level=RiskLevel.HIGH,
- idempotency_key_fields=("session_id", "event_id"),
- concurrency_class="device-write",
- conflict_keys=("device",),
- timeout_seconds=1.5,
- terminal=True,
- )
- registry = EventRegistry([definition])
- result = await EventKernel(registry).execute(
- EventRequest(
- id="event-1",
- name="example.lookup",
- arguments={"query": "value"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=["example.lookup"],
- )
- registered = registry.definition("example.lookup")
- assert registered is not definition
- assert result.result_policy is ResultPolicy.TEMPLATE_FOLLOW_UP
- assert result.confirmation_policy is ConfirmationPolicy.REQUIRED
- assert result.risk_level is RiskLevel.HIGH
- assert result.idempotency_key_fields == ("session_id", "event_id")
- assert result.concurrency_class == "device-write"
- assert result.conflict_keys == ("device",)
- assert result.timeout_seconds == 1.5
- assert result.terminal is True
- @pytest.mark.asyncio
- @pytest.mark.parametrize("name", ["alpha.one", "beta-two", "任意.事件"])
- async def test_kernel_applies_identical_behavior_to_arbitrary_event_names(name: str):
- definition = _definition(name=name)
- result = await EventKernel(EventRegistry([definition])).execute(
- EventRequest(
- id="event-1",
- name=name,
- arguments={"query": "value"},
- source=EventSource.PROVIDER_RESOLVED,
- ),
- enabled_names=[name],
- )
- assert result.status is EventStatus.SUCCESS
- assert result.payload == {"event": name, "query": "value"}
|