test_event_kernel.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408
  1. from __future__ import annotations
  2. import gc
  3. import json
  4. import warnings
  5. from dataclasses import FrozenInstanceError
  6. from typing import Any
  7. import pytest
  8. from agent_lab.application.events import (
  9. ConfirmationPolicy,
  10. EventDefinition,
  11. EventExecutionContext,
  12. EventKernel,
  13. EventRegistry,
  14. EventRequest,
  15. ResolvedEventArguments,
  16. EventSource,
  17. EventStatus,
  18. ResultPolicy,
  19. RiskLevel,
  20. )
  21. from agent_lab.application.events.models import EventArgumentResolution
  22. from agent_lab.application.tools import (
  23. ToolDefinition,
  24. ToolExecutionContext,
  25. ToolRegistry,
  26. )
  27. from agent_lab.domain.events import ToolCallEvent
  28. from agent_lab.domain.messages import ChatMessage
  29. def _definition(
  30. name: str = "example.lookup",
  31. **overrides: Any,
  32. ) -> EventDefinition:
  33. values: dict[str, Any] = {
  34. "name": name,
  35. "description": "Look up an example value.",
  36. "parameters": {
  37. "type": "object",
  38. "properties": {"query": {"type": "string"}},
  39. "required": ["query"],
  40. },
  41. "handler": lambda request: {
  42. "event": request.name,
  43. "query": request.arguments["query"],
  44. },
  45. }
  46. values.update(overrides)
  47. return EventDefinition(**values)
  48. def test_registry_registers_flat_definitions_and_filters_enabled_catalog():
  49. registry = EventRegistry(
  50. [_definition("example.lookup"), _definition("device.inspect")]
  51. )
  52. assert [item["name"] for item in registry.catalog()] == [
  53. "example.lookup",
  54. "device.inspect",
  55. ]
  56. assert registry.catalog(["device.inspect"]) == [
  57. {
  58. "name": "device.inspect",
  59. "description": "Look up an example value.",
  60. "parameters": {
  61. "type": "object",
  62. "properties": {"query": {"type": "string"}},
  63. "required": ["query"],
  64. },
  65. }
  66. ]
  67. assert registry.tool_schema("example.lookup")["function"]["name"] == (
  68. "example.lookup"
  69. )
  70. assert registry.tool_schema("missing") is None
  71. def test_registry_rejects_duplicate_definition_names():
  72. with pytest.raises(ValueError, match="duplicate event definition: example.lookup"):
  73. EventRegistry([_definition(), _definition()])
  74. def test_registry_rejects_invalid_draft_2020_12_schema():
  75. definition = _definition(parameters={"type": 42})
  76. with pytest.raises(ValueError, match="invalid event schema for example.lookup"):
  77. EventRegistry([definition])
  78. def test_registry_does_not_expose_mutable_validator_instances():
  79. registry = EventRegistry([_definition()])
  80. assert not hasattr(registry, "validator")
  81. errors = list(
  82. registry.iter_validation_errors(
  83. "example.lookup",
  84. {"query": 42},
  85. )
  86. )
  87. assert errors
  88. assert not hasattr(errors[0], "schema")
  89. assert not hasattr(errors[0], "instance")
  90. with pytest.raises(FrozenInstanceError):
  91. errors[0].message = "mutated" # type: ignore[misc]
  92. assert list(
  93. registry.iter_validation_errors(
  94. "example.lookup",
  95. {"query": 42},
  96. )
  97. )
  98. def test_tool_registry_public_api_remains_compatible():
  99. registry = ToolRegistry(
  100. [
  101. ToolDefinition(
  102. name="compat.lookup",
  103. description="Look up compatibility data.",
  104. parameters={
  105. "type": "object",
  106. "properties": {"query": {"type": "string"}},
  107. "required": ["query"],
  108. },
  109. handler=lambda event: {
  110. "tool": event.name,
  111. "query": event.arguments["query"],
  112. },
  113. argument_resolver=lambda event, context: {
  114. "query": context.history[-1].content
  115. },
  116. )
  117. ]
  118. )
  119. event = ToolCallEvent(
  120. id="call-1",
  121. name="compat.lookup",
  122. arguments={"query": "provider"},
  123. raw_arguments='{"query":"provider"}',
  124. )
  125. assert registry.available_tools() == [
  126. {
  127. "name": "compat.lookup",
  128. "description": "Look up compatibility data.",
  129. "parameters": {
  130. "type": "object",
  131. "properties": {"query": {"type": "string"}},
  132. "required": ["query"],
  133. },
  134. }
  135. ]
  136. assert "- compat.lookup: Look up compatibility data." in (
  137. registry.chat_event_system_message(["compat.lookup"])
  138. )
  139. assert registry.tool_schema("compat.lookup")["function"]["name"] == (
  140. "compat.lookup"
  141. )
  142. assert registry.handle(
  143. event,
  144. ToolExecutionContext(history=[ChatMessage(role="user", content="history")]),
  145. ) == {"tool": "compat.lookup", "query": "history"}
  146. assert registry.execute(event) == {"tool": "compat.lookup", "query": "provider"}
  147. @pytest.mark.asyncio
  148. async def test_kernel_executes_complete_deterministic_arguments_without_fallback():
  149. fallback_calls: list[str] = []
  150. async def fallback(*args: Any) -> dict[str, Any]:
  151. fallback_calls.append("called")
  152. return {"query": "fallback"}
  153. registry = EventRegistry(
  154. [_definition(resolver=lambda request, context: {"query": "deterministic"})]
  155. )
  156. result = await EventKernel(registry, argument_fallback=fallback).execute(
  157. EventRequest(id="event-1", name="example.lookup"),
  158. enabled_names=["example.lookup"],
  159. )
  160. assert result.status is EventStatus.SUCCESS
  161. assert result.arguments == {"query": "deterministic"}
  162. assert result.payload == {"event": "example.lookup", "query": "deterministic"}
  163. assert result.used_fallback is False
  164. assert fallback_calls == []
  165. @pytest.mark.asyncio
  166. async def test_kernel_calls_fallback_once_when_required_arguments_are_incomplete():
  167. fallback_calls: list[dict[str, Any]] = []
  168. async def fallback(
  169. definition: EventDefinition,
  170. request: EventRequest,
  171. context: EventExecutionContext,
  172. ) -> ResolvedEventArguments:
  173. fallback_calls.append(dict(request.arguments))
  174. return ResolvedEventArguments(
  175. event_name=definition.name,
  176. arguments={"query": "resolved once"},
  177. raw_arguments='{"query":"resolved once"}',
  178. )
  179. registry = EventRegistry([_definition(resolver=lambda request, context: {})])
  180. result = await EventKernel(registry, argument_fallback=fallback).execute(
  181. EventRequest(id="event-1", name="example.lookup"),
  182. enabled_names=["example.lookup"],
  183. )
  184. assert result.status is EventStatus.SUCCESS
  185. assert result.arguments == {"query": "resolved once"}
  186. assert result.used_fallback is True
  187. assert fallback_calls == [{}]
  188. @pytest.mark.asyncio
  189. @pytest.mark.parametrize(
  190. "arguments",
  191. [
  192. {"query": 42},
  193. {"query": "unsupported"},
  194. {"query": "valid", "unexpected": True},
  195. ],
  196. )
  197. async def test_kernel_does_not_fallback_for_complete_invalid_arguments(
  198. arguments: dict[str, Any],
  199. ):
  200. fallback_calls = 0
  201. async def fallback(*args: Any) -> ResolvedEventArguments:
  202. nonlocal fallback_calls
  203. fallback_calls += 1
  204. return ResolvedEventArguments(
  205. event_name="example.lookup",
  206. arguments={"query": "valid"},
  207. raw_arguments='{"query":"valid"}',
  208. )
  209. definition = _definition(
  210. parameters={
  211. "type": "object",
  212. "properties": {"query": {"type": "string", "enum": ["valid"]}},
  213. "required": ["query"],
  214. "additionalProperties": False,
  215. },
  216. resolver=lambda request, context: arguments,
  217. )
  218. result = await EventKernel(
  219. EventRegistry([definition]), argument_fallback=fallback
  220. ).execute(
  221. EventRequest(id="event-1", name="example.lookup"),
  222. enabled_names=["example.lookup"],
  223. )
  224. assert result.status is EventStatus.INVALID_ARGUMENTS
  225. assert result.used_fallback is False
  226. assert fallback_calls == 0
  227. def _discriminated_composed_parameters(composition: str) -> dict[str, Any]:
  228. return {
  229. "type": "object",
  230. "properties": {
  231. "choice": {
  232. composition: [
  233. {
  234. "type": "object",
  235. "properties": {
  236. "kind": {"const": "a"},
  237. "value": {"type": "string"},
  238. },
  239. "required": ["kind", "value"],
  240. },
  241. {
  242. "type": "object",
  243. "properties": {
  244. "kind": {"const": "b"},
  245. "count": {"type": "integer"},
  246. },
  247. "required": ["kind", "count"],
  248. },
  249. ]
  250. }
  251. },
  252. "required": ["choice"],
  253. }
  254. @pytest.mark.asyncio
  255. @pytest.mark.parametrize("composition", ["anyOf", "oneOf"])
  256. async def test_kernel_falls_back_for_matching_composed_branch_missing_required(
  257. composition: str,
  258. ):
  259. fallback_calls = 0
  260. async def fallback(*args: Any) -> ResolvedEventArguments:
  261. nonlocal fallback_calls
  262. fallback_calls += 1
  263. return ResolvedEventArguments(
  264. event_name="example.lookup",
  265. arguments={"choice": {"kind": "a", "value": "resolved"}},
  266. raw_arguments='{"choice":{"kind":"a","value":"resolved"}}',
  267. )
  268. definition = _definition(
  269. parameters=_discriminated_composed_parameters(composition),
  270. resolver=lambda request, context: {"choice": {"kind": "a"}},
  271. handler=lambda request: {"ok": True},
  272. )
  273. result = await EventKernel(
  274. EventRegistry([definition]), argument_fallback=fallback
  275. ).execute(
  276. EventRequest(id="event-1", name="example.lookup"),
  277. enabled_names=["example.lookup"],
  278. )
  279. assert result.status is EventStatus.SUCCESS
  280. assert result.used_fallback is True
  281. assert fallback_calls == 1
  282. @pytest.mark.asyncio
  283. @pytest.mark.parametrize("composition", ["anyOf", "oneOf"])
  284. async def test_kernel_does_not_fallback_when_matching_composed_branch_is_invalid(
  285. composition: str,
  286. ):
  287. fallback_calls = 0
  288. async def fallback(*args: Any) -> ResolvedEventArguments:
  289. nonlocal fallback_calls
  290. fallback_calls += 1
  291. raise AssertionError("fallback should not run")
  292. definition = _definition(
  293. parameters=_discriminated_composed_parameters(composition),
  294. resolver=lambda request, context: {
  295. "choice": {"kind": "a", "value": 42}
  296. },
  297. handler=lambda request: {"ok": True},
  298. )
  299. result = await EventKernel(
  300. EventRegistry([definition]), argument_fallback=fallback
  301. ).execute(
  302. EventRequest(id="event-1", name="example.lookup"),
  303. enabled_names=["example.lookup"],
  304. )
  305. assert result.status is EventStatus.INVALID_ARGUMENTS
  306. assert result.used_fallback is False
  307. assert fallback_calls == 0
  308. @pytest.mark.asyncio
  309. @pytest.mark.parametrize("composition", ["anyOf", "oneOf"])
  310. @pytest.mark.parametrize("choice", [{}, {"kind": "other"}])
  311. async def test_kernel_does_not_fallback_when_composed_branch_is_ambiguous(
  312. composition: str,
  313. choice: dict[str, Any],
  314. ):
  315. fallback_calls = 0
  316. async def fallback(*args: Any) -> ResolvedEventArguments:
  317. nonlocal fallback_calls
  318. fallback_calls += 1
  319. raise AssertionError("fallback should not run")
  320. definition = _definition(
  321. parameters=_discriminated_composed_parameters(composition),
  322. resolver=lambda request, context: {"choice": choice},
  323. handler=lambda request: {"ok": True},
  324. )
  325. result = await EventKernel(
  326. EventRegistry([definition]), argument_fallback=fallback
  327. ).execute(
  328. EventRequest(id="event-1", name="example.lookup"),
  329. enabled_names=["example.lookup"],
  330. )
  331. assert result.status is EventStatus.INVALID_ARGUMENTS
  332. assert result.used_fallback is False
  333. assert fallback_calls == 0
  334. @pytest.mark.asyncio
  335. async def test_kernel_does_not_fallback_when_definition_disallows_it():
  336. fallback_calls = 0
  337. async def fallback(*args: Any) -> ResolvedEventArguments:
  338. nonlocal fallback_calls
  339. fallback_calls += 1
  340. return ResolvedEventArguments(
  341. event_name="example.lookup",
  342. arguments={"query": "not allowed"},
  343. raw_arguments='{"query":"not allowed"}',
  344. )
  345. registry = EventRegistry(
  346. [
  347. _definition(
  348. resolver=lambda request, context: {},
  349. fallback_allowed=False,
  350. )
  351. ]
  352. )
  353. result = await EventKernel(registry, argument_fallback=fallback).execute(
  354. EventRequest(id="event-1", name="example.lookup"),
  355. enabled_names=["example.lookup"],
  356. )
  357. assert result.status is EventStatus.INVALID_ARGUMENTS
  358. assert result.error == "missing required arguments: query"
  359. assert result.used_fallback is False
  360. assert fallback_calls == 0
  361. @pytest.mark.asyncio
  362. async def test_structured_resolution_can_mark_optional_arguments_incomplete():
  363. fallback_calls = 0
  364. async def fallback(*args: Any) -> ResolvedEventArguments:
  365. nonlocal fallback_calls
  366. fallback_calls += 1
  367. return ResolvedEventArguments(
  368. event_name="example.lookup",
  369. arguments={"query": "resolved"},
  370. raw_arguments='{"query":"resolved"}',
  371. )
  372. definition = _definition(
  373. parameters={
  374. "type": "object",
  375. "properties": {"query": {"type": "string"}},
  376. "additionalProperties": False,
  377. },
  378. resolver=lambda request, context: EventArgumentResolution(
  379. arguments={},
  380. complete=False,
  381. ),
  382. )
  383. result = await EventKernel(
  384. EventRegistry([definition]), argument_fallback=fallback
  385. ).execute(
  386. EventRequest(id="event-1", name="example.lookup"),
  387. enabled_names=["example.lookup"],
  388. )
  389. assert result.status is EventStatus.SUCCESS
  390. assert result.arguments == {"query": "resolved"}
  391. assert result.used_fallback is True
  392. assert fallback_calls == 1
  393. @pytest.mark.asyncio
  394. async def test_structured_incomplete_optional_arguments_fail_without_fallback():
  395. definition = _definition(
  396. parameters={
  397. "type": "object",
  398. "properties": {"query": {"type": "string"}},
  399. },
  400. resolver=lambda request, context: EventArgumentResolution(
  401. arguments={},
  402. complete=False,
  403. ),
  404. handler=lambda request: {"ok": True},
  405. )
  406. result = await EventKernel(EventRegistry([definition])).execute(
  407. EventRequest(id="event-1", name="example.lookup"),
  408. enabled_names=["example.lookup"],
  409. )
  410. assert result.status is EventStatus.INVALID_ARGUMENTS
  411. assert result.error == "event arguments incomplete"
  412. @pytest.mark.asyncio
  413. async def test_plain_dict_resolution_remains_complete_for_optional_schema():
  414. fallback_calls = 0
  415. async def fallback(*args: Any) -> ResolvedEventArguments:
  416. nonlocal fallback_calls
  417. fallback_calls += 1
  418. raise AssertionError("fallback should not run")
  419. definition = _definition(
  420. parameters={
  421. "type": "object",
  422. "properties": {"query": {"type": "string"}},
  423. },
  424. resolver=lambda request, context: {},
  425. handler=lambda request: {"ok": True},
  426. )
  427. result = await EventKernel(
  428. EventRegistry([definition]), argument_fallback=fallback
  429. ).execute(
  430. EventRequest(id="event-1", name="example.lookup"),
  431. enabled_names=["example.lookup"],
  432. )
  433. assert result.status is EventStatus.SUCCESS
  434. assert fallback_calls == 0
  435. def test_sync_kernel_rejects_structured_incomplete_optional_arguments():
  436. handler_calls = 0
  437. def handler(request: EventRequest) -> dict[str, Any]:
  438. nonlocal handler_calls
  439. handler_calls += 1
  440. return {"ok": True}
  441. definition = _definition(
  442. parameters={
  443. "type": "object",
  444. "properties": {"query": {"type": "string"}},
  445. },
  446. resolver=lambda request, context: EventArgumentResolution(
  447. arguments={},
  448. complete=False,
  449. ),
  450. handler=handler,
  451. )
  452. result = EventKernel(EventRegistry([definition])).execute_sync(
  453. EventRequest(id="event-1", name="example.lookup"),
  454. enabled_names=["example.lookup"],
  455. )
  456. assert result.status is EventStatus.INVALID_ARGUMENTS
  457. assert result.error == "event arguments incomplete"
  458. assert handler_calls == 0
  459. @pytest.mark.parametrize(
  460. "resolver",
  461. [
  462. lambda request, context: EventArgumentResolution(
  463. arguments={},
  464. complete=True,
  465. ),
  466. lambda request, context: {},
  467. ],
  468. )
  469. def test_sync_kernel_preserves_complete_compatible_resolvers(resolver: Any):
  470. handler_calls = 0
  471. def handler(request: EventRequest) -> dict[str, Any]:
  472. nonlocal handler_calls
  473. handler_calls += 1
  474. return {"ok": True}
  475. definition = _definition(
  476. parameters={
  477. "type": "object",
  478. "properties": {"query": {"type": "string"}},
  479. },
  480. resolver=resolver,
  481. handler=handler,
  482. )
  483. result = EventKernel(EventRegistry([definition])).execute_sync(
  484. EventRequest(id="event-1", name="example.lookup"),
  485. enabled_names=["example.lookup"],
  486. )
  487. assert result.status is EventStatus.SUCCESS
  488. assert result.payload == {"ok": True}
  489. assert handler_calls == 1
  490. @pytest.mark.asyncio
  491. async def test_provider_resolved_arguments_are_not_rewritten_or_fallen_back():
  492. resolver_calls = 0
  493. fallback_calls = 0
  494. def resolver(*args: Any) -> dict[str, Any]:
  495. nonlocal resolver_calls
  496. resolver_calls += 1
  497. return {"query": "rewritten"}
  498. async def fallback(*args: Any) -> ResolvedEventArguments:
  499. nonlocal fallback_calls
  500. fallback_calls += 1
  501. return ResolvedEventArguments(
  502. event_name="example.lookup",
  503. arguments={"query": "fallback"},
  504. raw_arguments='{"query":"fallback"}',
  505. )
  506. registry = EventRegistry([_definition(resolver=resolver)])
  507. result = await EventKernel(registry, argument_fallback=fallback).execute(
  508. EventRequest(
  509. id="event-1",
  510. name="example.lookup",
  511. arguments={"query": "provider value"},
  512. source=EventSource.PROVIDER_RESOLVED,
  513. ),
  514. enabled_names=["example.lookup"],
  515. )
  516. assert result.status is EventStatus.SUCCESS
  517. assert result.arguments == {"query": "provider value"}
  518. assert resolver_calls == 0
  519. assert fallback_calls == 0
  520. @pytest.mark.asyncio
  521. async def test_provider_resolved_missing_arguments_return_invalid_without_fallback():
  522. fallback_calls = 0
  523. async def fallback(*args: Any) -> ResolvedEventArguments:
  524. nonlocal fallback_calls
  525. fallback_calls += 1
  526. return ResolvedEventArguments(
  527. event_name="example.lookup",
  528. arguments={"query": "fallback"},
  529. raw_arguments='{"query":"fallback"}',
  530. )
  531. result = await EventKernel(
  532. EventRegistry([_definition()]), argument_fallback=fallback
  533. ).execute(
  534. EventRequest(
  535. id="event-1",
  536. name="example.lookup",
  537. arguments={},
  538. source=EventSource.PROVIDER_RESOLVED,
  539. ),
  540. enabled_names=["example.lookup"],
  541. )
  542. assert result.status is EventStatus.INVALID_ARGUMENTS
  543. assert result.error == "missing required arguments: query"
  544. assert fallback_calls == 0
  545. @pytest.mark.asyncio
  546. @pytest.mark.parametrize(
  547. ("event_request", "enabled_names", "expected_status", "expected_error"),
  548. [
  549. (
  550. EventRequest(id="event-1", name="missing"),
  551. ["missing"],
  552. EventStatus.UNKNOWN,
  553. "unknown event",
  554. ),
  555. (
  556. EventRequest(id="event-1", name="example.lookup"),
  557. [],
  558. EventStatus.DISABLED,
  559. "event disabled",
  560. ),
  561. (
  562. EventRequest(
  563. id="event-1",
  564. name="example.lookup",
  565. arguments={"query": 42},
  566. source=EventSource.PROVIDER_RESOLVED,
  567. ),
  568. ["example.lookup"],
  569. EventStatus.INVALID_ARGUMENTS,
  570. "invalid argument type for query: expected string",
  571. ),
  572. ],
  573. )
  574. async def test_kernel_normalizes_lookup_and_validation_failures(
  575. event_request: EventRequest,
  576. enabled_names: list[str],
  577. expected_status: EventStatus,
  578. expected_error: str,
  579. ):
  580. result = await EventKernel(EventRegistry([_definition()])).execute(
  581. event_request,
  582. enabled_names=enabled_names,
  583. )
  584. assert result.status is expected_status
  585. assert result.error == expected_error
  586. @pytest.mark.asyncio
  587. async def test_kernel_normalizes_handler_exceptions():
  588. def fail(request: EventRequest) -> dict[str, Any]:
  589. raise RuntimeError("boom")
  590. result = await EventKernel(
  591. EventRegistry([_definition(handler=fail)])
  592. ).execute(
  593. EventRequest(
  594. id="event-1",
  595. name="example.lookup",
  596. arguments={"query": "value"},
  597. source=EventSource.PROVIDER_RESOLVED,
  598. ),
  599. enabled_names=["example.lookup"],
  600. )
  601. assert result.status is EventStatus.HANDLER_ERROR
  602. assert result.error == "event handler failed: boom"
  603. @pytest.mark.asyncio
  604. async def test_kernel_validates_complete_draft_2020_12_schema():
  605. definition = _definition(
  606. parameters={
  607. "type": "object",
  608. "properties": {
  609. "mode": {"enum": ["quick", "deep"]},
  610. "target": {"type": ["string", "null"]},
  611. "filters": {
  612. "type": "array",
  613. "items": {
  614. "type": "object",
  615. "properties": {"score": {"type": "number", "minimum": 0}},
  616. "required": ["score"],
  617. "additionalProperties": False,
  618. },
  619. },
  620. },
  621. "required": ["mode", "target", "filters"],
  622. "additionalProperties": False,
  623. },
  624. handler=lambda request: {"event": request.name},
  625. )
  626. kernel = EventKernel(EventRegistry([definition]))
  627. valid = await kernel.execute(
  628. EventRequest(
  629. id="valid",
  630. name=definition.name,
  631. arguments={
  632. "mode": "deep",
  633. "target": None,
  634. "filters": [{"score": 0.5}],
  635. },
  636. source=EventSource.PROVIDER_RESOLVED,
  637. ),
  638. enabled_names=[definition.name],
  639. )
  640. invalid = await kernel.execute(
  641. EventRequest(
  642. id="invalid",
  643. name=definition.name,
  644. arguments={
  645. "mode": "other",
  646. "target": 7,
  647. "filters": [{"score": -1, "extra": True}],
  648. "unexpected": True,
  649. },
  650. source=EventSource.PROVIDER_RESOLVED,
  651. ),
  652. enabled_names=[definition.name],
  653. )
  654. assert valid.status is EventStatus.SUCCESS
  655. assert invalid.status is EventStatus.INVALID_ARGUMENTS
  656. assert invalid.error.startswith("invalid event arguments:")
  657. @pytest.mark.asyncio
  658. async def test_kernel_normalizes_validator_runtime_exceptions():
  659. definition = _definition(parameters={"$ref": "urn:agent-lab:missing-schema"})
  660. result = await EventKernel(EventRegistry([definition])).execute(
  661. EventRequest(
  662. id="event-1",
  663. name=definition.name,
  664. arguments={},
  665. source=EventSource.PROVIDER_RESOLVED,
  666. ),
  667. enabled_names=[definition.name],
  668. )
  669. assert result.status is EventStatus.DEFINITION_ERROR
  670. assert result.error.startswith("event argument validation failed:")
  671. @pytest.mark.asyncio
  672. async def test_tool_registry_maps_definition_errors_to_tool_compatibility_payload():
  673. registry = ToolRegistry(
  674. [
  675. ToolDefinition(
  676. name="broken.lookup",
  677. description="Broken lookup.",
  678. parameters={"$ref": "urn:agent-lab:missing-schema"},
  679. handler=lambda event: {"tool": event.name},
  680. )
  681. ]
  682. )
  683. payload = await registry.execute_async(
  684. ToolCallEvent(
  685. id="call-1",
  686. name="broken.lookup",
  687. arguments={},
  688. raw_arguments="{}",
  689. )
  690. )
  691. assert payload["tool"] == "broken.lookup"
  692. assert payload["error"].startswith("tool definition validation failed:")
  693. @pytest.mark.asyncio
  694. @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
  695. async def test_kernel_normalizes_resolution_boundary_exceptions(boundary: str):
  696. def resolver(request: EventRequest, context: EventExecutionContext) -> dict[str, Any]:
  697. if boundary == "resolver":
  698. raise RuntimeError("resolver boom")
  699. return {}
  700. async def fallback(*args: Any) -> ResolvedEventArguments:
  701. raise RuntimeError("fallback boom")
  702. result = await EventKernel(
  703. EventRegistry([_definition(resolver=resolver)]),
  704. argument_fallback=fallback,
  705. ).execute(
  706. EventRequest(id="event-1", name="example.lookup"),
  707. enabled_names=["example.lookup"],
  708. )
  709. assert result.status is EventStatus.RESOLUTION_ERROR
  710. assert result.error == f"event argument {boundary} failed: {boundary} boom"
  711. @pytest.mark.asyncio
  712. @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
  713. async def test_kernel_normalizes_invalid_resolution_payloads(boundary: str):
  714. resolver = (
  715. (lambda request, context: None)
  716. if boundary == "resolver"
  717. else (lambda request, context: {})
  718. )
  719. async def fallback(*args: Any) -> Any:
  720. return {"query": "legacy bare mapping"}
  721. result = await EventKernel(
  722. EventRegistry([_definition(resolver=resolver)]),
  723. argument_fallback=fallback,
  724. ).execute(
  725. EventRequest(id="event-1", name="example.lookup"),
  726. enabled_names=["example.lookup"],
  727. )
  728. assert result.status is EventStatus.RESOLUTION_ERROR
  729. assert result.error == f"event argument {boundary} returned invalid payload"
  730. @pytest.mark.asyncio
  731. async def test_kernel_normalizes_non_json_resolver_arguments():
  732. result = await EventKernel(
  733. EventRegistry(
  734. [
  735. _definition(
  736. resolver=lambda request, context: {"query": object()}
  737. )
  738. ]
  739. )
  740. ).execute(
  741. EventRequest(id="event-1", name="example.lookup"),
  742. enabled_names=["example.lookup"],
  743. )
  744. assert result.status is EventStatus.RESOLUTION_ERROR
  745. assert result.error.startswith("event argument resolver failed to serialize:")
  746. class _ExplodingDeepcopyDict(dict[str, Any]):
  747. def __deepcopy__(self, memo: dict[int, Any]) -> dict[str, Any]:
  748. raise RuntimeError("deepcopy must not be used")
  749. @pytest.mark.asyncio
  750. @pytest.mark.parametrize("execution", ["async", "sync"])
  751. @pytest.mark.parametrize(
  752. ("source", "expected_status", "expected_error"),
  753. [
  754. (
  755. EventSource.TEXT_EVENT,
  756. EventStatus.RESOLUTION_ERROR,
  757. "event argument resolver failed to serialize: "
  758. "JSON round-trip changed payload",
  759. ),
  760. (
  761. EventSource.PROVIDER_RESOLVED,
  762. EventStatus.INVALID_ARGUMENTS,
  763. "provider-resolved event arguments are not valid JSON",
  764. ),
  765. ],
  766. )
  767. async def test_kernel_normalizes_argument_snapshot_failures_without_deepcopy(
  768. execution: str,
  769. source: EventSource,
  770. expected_status: EventStatus,
  771. expected_error: str,
  772. ):
  773. fallback_calls = 0
  774. arguments = {"query": _ExplodingDeepcopyDict({"nested": "value"})}
  775. async def fallback(*args: Any) -> ResolvedEventArguments:
  776. nonlocal fallback_calls
  777. fallback_calls += 1
  778. raise AssertionError("fallback should not run")
  779. definition = _definition(
  780. parameters={
  781. "type": "object",
  782. "properties": {"query": {"type": "object"}},
  783. "required": ["query"],
  784. },
  785. resolver=lambda request, context: arguments,
  786. handler=lambda request: {"ok": True},
  787. )
  788. request = EventRequest(
  789. id="event-1",
  790. name="example.lookup",
  791. arguments=arguments if source is EventSource.PROVIDER_RESOLVED else {},
  792. source=source,
  793. )
  794. kernel = EventKernel(EventRegistry([definition]), argument_fallback=fallback)
  795. result = (
  796. await kernel.execute(request, enabled_names=[definition.name])
  797. if execution == "async"
  798. else kernel.execute_sync(request, enabled_names=[definition.name])
  799. )
  800. assert result.status is expected_status
  801. assert result.status is not EventStatus.DEFINITION_ERROR
  802. assert result.error == expected_error
  803. assert result.used_fallback is False
  804. assert fallback_calls == 0
  805. @pytest.mark.asyncio
  806. async def test_kernel_normalizes_non_object_fallback_arguments():
  807. async def fallback(*args: Any) -> ResolvedEventArguments:
  808. return ResolvedEventArguments(
  809. event_name="example.lookup",
  810. arguments=["not", "an", "object"], # type: ignore[arg-type]
  811. raw_arguments='["not","an","object"]',
  812. )
  813. result = await EventKernel(
  814. EventRegistry([_definition(resolver=lambda request, context: {})]),
  815. argument_fallback=fallback,
  816. ).execute(
  817. EventRequest(id="event-1", name="example.lookup"),
  818. enabled_names=["example.lookup"],
  819. )
  820. assert result.status is EventStatus.RESOLUTION_ERROR
  821. assert result.error == "event argument fallback returned invalid payload"
  822. @pytest.mark.asyncio
  823. @pytest.mark.parametrize(
  824. ("resolved", "expected_error"),
  825. [
  826. (
  827. ResolvedEventArguments(
  828. event_name="another.event",
  829. arguments={"query": "value"},
  830. raw_arguments='{"query":"value"}',
  831. ),
  832. "fallback returned tool another.event for example.lookup",
  833. ),
  834. (
  835. ResolvedEventArguments(
  836. event_name="example.lookup",
  837. arguments={"query": "value"},
  838. raw_arguments="not-json",
  839. ),
  840. "fallback raw arguments are not valid JSON",
  841. ),
  842. (
  843. ResolvedEventArguments(
  844. event_name="example.lookup",
  845. arguments={"query": "parsed"},
  846. raw_arguments='{"query":"raw"}',
  847. ),
  848. "fallback raw arguments do not match parsed arguments",
  849. ),
  850. ],
  851. )
  852. async def test_kernel_rejects_inconsistent_structured_fallback(
  853. resolved: ResolvedEventArguments,
  854. expected_error: str,
  855. ):
  856. async def fallback(*args: Any) -> ResolvedEventArguments:
  857. return resolved
  858. result = await EventKernel(
  859. EventRegistry([_definition(resolver=lambda request, context: {})]),
  860. argument_fallback=fallback,
  861. ).execute(
  862. EventRequest(id="event-1", name="example.lookup"),
  863. enabled_names=["example.lookup"],
  864. )
  865. assert result.status is EventStatus.RESOLUTION_ERROR
  866. assert result.error == expected_error
  867. @pytest.mark.asyncio
  868. @pytest.mark.parametrize(
  869. ("arguments", "raw_arguments"),
  870. [
  871. ({"query": 1}, '{"query":true}'),
  872. ({"query": 1.0}, '{"query":1}'),
  873. ({"query": {"nested": [1]}}, '{"query":{"nested":[true]}}'),
  874. ({"query": float("nan")}, '{"query":NaN}'),
  875. ({"query": float("inf")}, '{"query":Infinity}'),
  876. ],
  877. )
  878. async def test_kernel_rejects_noncanonical_fallback_json(
  879. arguments: dict[str, Any],
  880. raw_arguments: str,
  881. ):
  882. async def fallback(*args: Any) -> ResolvedEventArguments:
  883. return ResolvedEventArguments(
  884. event_name="example.lookup",
  885. arguments=arguments,
  886. raw_arguments=raw_arguments,
  887. )
  888. result = await EventKernel(
  889. EventRegistry([_definition(resolver=lambda request, context: {})]),
  890. argument_fallback=fallback,
  891. ).execute(
  892. EventRequest(id="event-1", name="example.lookup"),
  893. enabled_names=["example.lookup"],
  894. )
  895. assert result.status is EventStatus.RESOLUTION_ERROR
  896. @pytest.mark.asyncio
  897. async def test_registry_schema_is_isolated_from_caller_mutation():
  898. parameters = {
  899. "type": "object",
  900. "properties": {"query": {"type": "string"}},
  901. "required": ["query"],
  902. "additionalProperties": False,
  903. }
  904. registry = EventRegistry([_definition(parameters=parameters)])
  905. parameters["properties"]["query"]["type"] = "integer"
  906. parameters["required"].clear()
  907. parameters["additionalProperties"] = True
  908. assert registry.catalog()[0]["parameters"] == {
  909. "type": "object",
  910. "properties": {"query": {"type": "string"}},
  911. "required": ["query"],
  912. "additionalProperties": False,
  913. }
  914. registered = registry.definition("example.lookup")
  915. assert registered is not None
  916. with pytest.raises(TypeError):
  917. registered.parameters["additionalProperties"] = True
  918. with pytest.raises(TypeError):
  919. registered.parameters["properties"]["query"]["type"] = "integer"
  920. with pytest.raises(AttributeError):
  921. registered.parameters["required"].append("unexpected")
  922. result = await EventKernel(registry).execute(
  923. EventRequest(
  924. id="event-1",
  925. name="example.lookup",
  926. arguments={"query": 42, "unexpected": True},
  927. source=EventSource.PROVIDER_RESOLVED,
  928. ),
  929. enabled_names=["example.lookup"],
  930. )
  931. assert result.status is EventStatus.INVALID_ARGUMENTS
  932. @pytest.mark.asyncio
  933. async def test_kernel_passes_consistent_fallback_arguments_and_raw_json_to_handler():
  934. captured: list[EventRequest] = []
  935. async def fallback(*args: Any) -> ResolvedEventArguments:
  936. return ResolvedEventArguments(
  937. event_name="example.lookup",
  938. arguments={"query": "resolved"},
  939. raw_arguments='{"query":"resolved"}',
  940. )
  941. result = await EventKernel(
  942. EventRegistry(
  943. [
  944. _definition(
  945. resolver=lambda request, context: {},
  946. handler=lambda request: captured.append(request) or {"ok": True},
  947. )
  948. ]
  949. ),
  950. argument_fallback=fallback,
  951. ).execute(
  952. EventRequest(id="event-1", name="example.lookup"),
  953. enabled_names=["example.lookup"],
  954. )
  955. assert result.status is EventStatus.SUCCESS
  956. assert captured[0].arguments == json.loads(captured[0].raw_arguments)
  957. assert result.raw_arguments == captured[0].raw_arguments
  958. @pytest.mark.asyncio
  959. @pytest.mark.parametrize("execution", ["async", "sync"])
  960. async def test_kernel_isolates_nested_handler_mutation_from_audit_values(
  961. execution: str,
  962. ):
  963. caller_arguments = {"nested": {"items": ["original"]}}
  964. handler_arguments: list[dict[str, Any]] = []
  965. def handler(request: EventRequest) -> dict[str, Any]:
  966. handler_arguments.append(request.arguments)
  967. request.arguments["nested"]["items"].append("handler")
  968. return {"nested": request.arguments["nested"]}
  969. definition = _definition(
  970. parameters={
  971. "type": "object",
  972. "properties": {
  973. "nested": {
  974. "type": "object",
  975. "properties": {
  976. "items": {"type": "array", "items": {"type": "string"}}
  977. },
  978. "required": ["items"],
  979. }
  980. },
  981. "required": ["nested"],
  982. },
  983. handler=handler,
  984. )
  985. request = EventRequest(
  986. id="event-1",
  987. name="example.lookup",
  988. arguments=caller_arguments,
  989. raw_arguments='{"nested":{"items":["original"]}}',
  990. source=EventSource.PROVIDER_RESOLVED,
  991. )
  992. kernel = EventKernel(EventRegistry([definition]))
  993. result = (
  994. await kernel.execute(request, enabled_names=["example.lookup"])
  995. if execution == "async"
  996. else kernel.execute_sync(request, enabled_names=["example.lookup"])
  997. )
  998. assert result.status is EventStatus.SUCCESS
  999. assert caller_arguments == {"nested": {"items": ["original"]}}
  1000. assert request.arguments == {"nested": {"items": ["original"]}}
  1001. assert result.arguments == {"nested": {"items": ["original"]}}
  1002. assert json.loads(result.raw_arguments) == result.arguments
  1003. assert result.payload == {"nested": {"items": ["original", "handler"]}}
  1004. handler_arguments[0]["nested"]["items"].append("later")
  1005. assert result.payload == {"nested": {"items": ["original", "handler"]}}
  1006. result.payload["nested"]["items"].append("result")
  1007. assert result.arguments == {"nested": {"items": ["original"]}}
  1008. assert caller_arguments == {"nested": {"items": ["original"]}}
  1009. @pytest.mark.asyncio
  1010. @pytest.mark.parametrize("payload", [None, "text", 1, ["item"]])
  1011. async def test_kernel_normalizes_invalid_handler_payloads(payload: Any):
  1012. result = await EventKernel(
  1013. EventRegistry([_definition(handler=lambda request: payload)])
  1014. ).execute(
  1015. EventRequest(
  1016. id="event-1",
  1017. name="example.lookup",
  1018. arguments={"query": "value"},
  1019. source=EventSource.PROVIDER_RESOLVED,
  1020. ),
  1021. enabled_names=["example.lookup"],
  1022. )
  1023. assert result.status is EventStatus.HANDLER_ERROR
  1024. assert result.error == "event handler returned non-object payload"
  1025. @pytest.mark.asyncio
  1026. @pytest.mark.parametrize("execution", ["async", "sync"])
  1027. @pytest.mark.parametrize(
  1028. "invalid_value",
  1029. [object(), {"set-item"}, float("nan"), float("inf")],
  1030. )
  1031. async def test_kernel_rejects_non_json_handler_dictionary_payloads(
  1032. execution: str,
  1033. invalid_value: Any,
  1034. ):
  1035. definition = _definition(
  1036. handler=lambda request: {"invalid": invalid_value},
  1037. )
  1038. request = EventRequest(
  1039. id="event-1",
  1040. name="example.lookup",
  1041. arguments={"query": "value"},
  1042. raw_arguments='{"query":"value"}',
  1043. source=EventSource.PROVIDER_RESOLVED,
  1044. )
  1045. kernel = EventKernel(EventRegistry([definition]))
  1046. result = (
  1047. await kernel.execute(request, enabled_names=["example.lookup"])
  1048. if execution == "async"
  1049. else kernel.execute_sync(request, enabled_names=["example.lookup"])
  1050. )
  1051. assert result.status is EventStatus.HANDLER_ERROR
  1052. assert result.error == "event handler returned non-JSON payload"
  1053. @pytest.mark.asyncio
  1054. async def test_async_kernel_supports_async_handler():
  1055. async def handler(request: EventRequest) -> dict[str, Any]:
  1056. return {"query": request.arguments["query"]}
  1057. result = await EventKernel(
  1058. EventRegistry([_definition(handler=handler)])
  1059. ).execute(
  1060. EventRequest(
  1061. id="event-1",
  1062. name="example.lookup",
  1063. arguments={"query": "async"},
  1064. source=EventSource.PROVIDER_RESOLVED,
  1065. ),
  1066. enabled_names=["example.lookup"],
  1067. )
  1068. assert result.status is EventStatus.SUCCESS
  1069. assert result.payload == {"query": "async"}
  1070. @pytest.mark.asyncio
  1071. async def test_tool_registry_async_entry_points_support_async_handler():
  1072. async def handler(event: ToolCallEvent) -> dict[str, Any]:
  1073. return {"tool": event.name, "query": event.arguments["query"]}
  1074. registry = ToolRegistry(
  1075. [
  1076. ToolDefinition(
  1077. name="async.lookup",
  1078. description="Async lookup.",
  1079. parameters={
  1080. "type": "object",
  1081. "properties": {"query": {"type": "string"}},
  1082. "required": ["query"],
  1083. },
  1084. handler=handler,
  1085. )
  1086. ]
  1087. )
  1088. event = ToolCallEvent(
  1089. id="call-1",
  1090. name="async.lookup",
  1091. arguments={"query": "value"},
  1092. raw_arguments='{"query":"value"}',
  1093. )
  1094. assert await registry.handle_async(event) == {
  1095. "tool": "async.lookup",
  1096. "query": "value",
  1097. }
  1098. assert await registry.execute_async(event) == {
  1099. "tool": "async.lookup",
  1100. "query": "value",
  1101. }
  1102. def test_tool_registry_sync_facade_rejects_async_handler_without_runtime_warning():
  1103. called = False
  1104. async def handler(event: ToolCallEvent) -> dict[str, Any]:
  1105. nonlocal called
  1106. called = True
  1107. return {"tool": event.name}
  1108. registry = ToolRegistry(
  1109. [
  1110. ToolDefinition(
  1111. name="async.lookup",
  1112. description="Async lookup.",
  1113. parameters={"type": "object"},
  1114. handler=handler,
  1115. )
  1116. ]
  1117. )
  1118. event = ToolCallEvent(
  1119. id="call-1",
  1120. name="async.lookup",
  1121. arguments={},
  1122. raw_arguments="{}",
  1123. )
  1124. with warnings.catch_warnings(record=True) as captured:
  1125. warnings.simplefilter("always")
  1126. payload = registry.execute(event)
  1127. gc.collect()
  1128. assert payload == {
  1129. "tool": "async.lookup",
  1130. "error": "tool handler failed: async event handlers require execute_async",
  1131. }
  1132. assert called is False
  1133. assert not [warning for warning in captured if issubclass(warning.category, RuntimeWarning)]
  1134. @pytest.mark.asyncio
  1135. async def test_kernel_rejects_boolean_for_json_number_arguments():
  1136. definition = _definition(
  1137. parameters={
  1138. "type": "object",
  1139. "properties": {"query": {"type": "number"}},
  1140. "required": ["query"],
  1141. }
  1142. )
  1143. result = await EventKernel(EventRegistry([definition])).execute(
  1144. EventRequest(
  1145. id="event-1",
  1146. name="example.lookup",
  1147. arguments={"query": True},
  1148. source=EventSource.PROVIDER_RESOLVED,
  1149. ),
  1150. enabled_names=["example.lookup"],
  1151. )
  1152. assert result.status is EventStatus.INVALID_ARGUMENTS
  1153. assert result.error == "invalid argument type for query: expected number"
  1154. @pytest.mark.asyncio
  1155. async def test_definition_metadata_survives_registration_and_result_creation():
  1156. definition = _definition(
  1157. result_policy=ResultPolicy.TEMPLATE_FOLLOW_UP,
  1158. confirmation_policy=ConfirmationPolicy.REQUIRED,
  1159. risk_level=RiskLevel.HIGH,
  1160. idempotency_key_fields=("session_id", "event_id"),
  1161. concurrency_class="device-write",
  1162. conflict_keys=("device",),
  1163. timeout_seconds=1.5,
  1164. terminal=True,
  1165. )
  1166. registry = EventRegistry([definition])
  1167. result = await EventKernel(registry).execute(
  1168. EventRequest(
  1169. id="event-1",
  1170. name="example.lookup",
  1171. arguments={"query": "value"},
  1172. source=EventSource.PROVIDER_RESOLVED,
  1173. ),
  1174. enabled_names=["example.lookup"],
  1175. )
  1176. registered = registry.definition("example.lookup")
  1177. assert registered is not definition
  1178. assert result.result_policy is ResultPolicy.TEMPLATE_FOLLOW_UP
  1179. assert result.confirmation_policy is ConfirmationPolicy.REQUIRED
  1180. assert result.risk_level is RiskLevel.HIGH
  1181. assert result.idempotency_key_fields == ("session_id", "event_id")
  1182. assert result.concurrency_class == "device-write"
  1183. assert result.conflict_keys == ("device",)
  1184. assert result.timeout_seconds == 1.5
  1185. assert result.terminal is True
  1186. @pytest.mark.asyncio
  1187. @pytest.mark.parametrize("name", ["alpha.one", "beta-two", "任意.事件"])
  1188. async def test_kernel_applies_identical_behavior_to_arbitrary_event_names(name: str):
  1189. definition = _definition(name=name)
  1190. result = await EventKernel(EventRegistry([definition])).execute(
  1191. EventRequest(
  1192. id="event-1",
  1193. name=name,
  1194. arguments={"query": "value"},
  1195. source=EventSource.PROVIDER_RESOLVED,
  1196. ),
  1197. enabled_names=[name],
  1198. )
  1199. assert result.status is EventStatus.SUCCESS
  1200. assert result.payload == {"event": name, "query": "value"}