test_event_kernel.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. from __future__ import annotations
  2. import gc
  3. import json
  4. import warnings
  5. from typing import Any
  6. import pytest
  7. from agent_lab.application.events import (
  8. ConfirmationPolicy,
  9. EventDefinition,
  10. EventExecutionContext,
  11. EventKernel,
  12. EventRegistry,
  13. EventRequest,
  14. ResolvedEventArguments,
  15. EventSource,
  16. EventStatus,
  17. ResultPolicy,
  18. RiskLevel,
  19. )
  20. from agent_lab.application.tools import (
  21. ToolDefinition,
  22. ToolExecutionContext,
  23. ToolRegistry,
  24. )
  25. from agent_lab.domain.events import ToolCallEvent
  26. from agent_lab.domain.messages import ChatMessage
  27. def _definition(
  28. name: str = "example.lookup",
  29. **overrides: Any,
  30. ) -> EventDefinition:
  31. values: dict[str, Any] = {
  32. "name": name,
  33. "description": "Look up an example value.",
  34. "parameters": {
  35. "type": "object",
  36. "properties": {"query": {"type": "string"}},
  37. "required": ["query"],
  38. },
  39. "handler": lambda request: {
  40. "event": request.name,
  41. "query": request.arguments["query"],
  42. },
  43. }
  44. values.update(overrides)
  45. return EventDefinition(**values)
  46. def test_registry_registers_flat_definitions_and_filters_enabled_catalog():
  47. registry = EventRegistry(
  48. [_definition("example.lookup"), _definition("device.inspect")]
  49. )
  50. assert [item["name"] for item in registry.catalog()] == [
  51. "example.lookup",
  52. "device.inspect",
  53. ]
  54. assert registry.catalog(["device.inspect"]) == [
  55. {
  56. "name": "device.inspect",
  57. "description": "Look up an example value.",
  58. "parameters": {
  59. "type": "object",
  60. "properties": {"query": {"type": "string"}},
  61. "required": ["query"],
  62. },
  63. }
  64. ]
  65. assert registry.tool_schema("example.lookup")["function"]["name"] == (
  66. "example.lookup"
  67. )
  68. assert registry.tool_schema("missing") is None
  69. def test_registry_rejects_duplicate_definition_names():
  70. with pytest.raises(ValueError, match="duplicate event definition: example.lookup"):
  71. EventRegistry([_definition(), _definition()])
  72. def test_registry_rejects_invalid_draft_2020_12_schema():
  73. definition = _definition(parameters={"type": 42})
  74. with pytest.raises(ValueError, match="invalid event schema for example.lookup"):
  75. EventRegistry([definition])
  76. def test_tool_registry_public_api_remains_compatible():
  77. registry = ToolRegistry(
  78. [
  79. ToolDefinition(
  80. name="compat.lookup",
  81. description="Look up compatibility data.",
  82. parameters={
  83. "type": "object",
  84. "properties": {"query": {"type": "string"}},
  85. "required": ["query"],
  86. },
  87. handler=lambda event: {
  88. "tool": event.name,
  89. "query": event.arguments["query"],
  90. },
  91. argument_resolver=lambda event, context: {
  92. "query": context.history[-1].content
  93. },
  94. )
  95. ]
  96. )
  97. event = ToolCallEvent(
  98. id="call-1",
  99. name="compat.lookup",
  100. arguments={"query": "provider"},
  101. raw_arguments='{"query":"provider"}',
  102. )
  103. assert registry.available_tools() == [
  104. {
  105. "name": "compat.lookup",
  106. "description": "Look up compatibility data.",
  107. "parameters": {
  108. "type": "object",
  109. "properties": {"query": {"type": "string"}},
  110. "required": ["query"],
  111. },
  112. }
  113. ]
  114. assert "- compat.lookup: Look up compatibility data." in (
  115. registry.chat_event_system_message(["compat.lookup"])
  116. )
  117. assert registry.tool_schema("compat.lookup")["function"]["name"] == (
  118. "compat.lookup"
  119. )
  120. assert registry.handle(
  121. event,
  122. ToolExecutionContext(history=[ChatMessage(role="user", content="history")]),
  123. ) == {"tool": "compat.lookup", "query": "history"}
  124. assert registry.execute(event) == {"tool": "compat.lookup", "query": "provider"}
  125. @pytest.mark.asyncio
  126. async def test_kernel_executes_complete_deterministic_arguments_without_fallback():
  127. fallback_calls: list[str] = []
  128. async def fallback(*args: Any) -> dict[str, Any]:
  129. fallback_calls.append("called")
  130. return {"query": "fallback"}
  131. registry = EventRegistry(
  132. [_definition(resolver=lambda request, context: {"query": "deterministic"})]
  133. )
  134. result = await EventKernel(registry, argument_fallback=fallback).execute(
  135. EventRequest(id="event-1", name="example.lookup"),
  136. enabled_names=["example.lookup"],
  137. )
  138. assert result.status is EventStatus.SUCCESS
  139. assert result.arguments == {"query": "deterministic"}
  140. assert result.payload == {"event": "example.lookup", "query": "deterministic"}
  141. assert result.used_fallback is False
  142. assert fallback_calls == []
  143. @pytest.mark.asyncio
  144. async def test_kernel_calls_fallback_once_when_required_arguments_are_incomplete():
  145. fallback_calls: list[dict[str, Any]] = []
  146. async def fallback(
  147. definition: EventDefinition,
  148. request: EventRequest,
  149. context: EventExecutionContext,
  150. ) -> ResolvedEventArguments:
  151. fallback_calls.append(dict(request.arguments))
  152. return ResolvedEventArguments(
  153. event_name=definition.name,
  154. arguments={"query": "resolved once"},
  155. raw_arguments='{"query":"resolved once"}',
  156. )
  157. registry = EventRegistry([_definition(resolver=lambda request, context: {})])
  158. result = await EventKernel(registry, argument_fallback=fallback).execute(
  159. EventRequest(id="event-1", name="example.lookup"),
  160. enabled_names=["example.lookup"],
  161. )
  162. assert result.status is EventStatus.SUCCESS
  163. assert result.arguments == {"query": "resolved once"}
  164. assert result.used_fallback is True
  165. assert fallback_calls == [{}]
  166. @pytest.mark.asyncio
  167. async def test_kernel_does_not_fallback_when_definition_disallows_it():
  168. fallback_calls = 0
  169. async def fallback(*args: Any) -> ResolvedEventArguments:
  170. nonlocal fallback_calls
  171. fallback_calls += 1
  172. return ResolvedEventArguments(
  173. event_name="example.lookup",
  174. arguments={"query": "not allowed"},
  175. raw_arguments='{"query":"not allowed"}',
  176. )
  177. registry = EventRegistry(
  178. [
  179. _definition(
  180. resolver=lambda request, context: {},
  181. fallback_allowed=False,
  182. )
  183. ]
  184. )
  185. result = await EventKernel(registry, argument_fallback=fallback).execute(
  186. EventRequest(id="event-1", name="example.lookup"),
  187. enabled_names=["example.lookup"],
  188. )
  189. assert result.status is EventStatus.INVALID_ARGUMENTS
  190. assert result.error == "missing required arguments: query"
  191. assert result.used_fallback is False
  192. assert fallback_calls == 0
  193. @pytest.mark.asyncio
  194. async def test_provider_resolved_arguments_are_not_rewritten_or_fallen_back():
  195. resolver_calls = 0
  196. fallback_calls = 0
  197. def resolver(*args: Any) -> dict[str, Any]:
  198. nonlocal resolver_calls
  199. resolver_calls += 1
  200. return {"query": "rewritten"}
  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": "fallback"},
  207. raw_arguments='{"query":"fallback"}',
  208. )
  209. registry = EventRegistry([_definition(resolver=resolver)])
  210. result = await EventKernel(registry, argument_fallback=fallback).execute(
  211. EventRequest(
  212. id="event-1",
  213. name="example.lookup",
  214. arguments={"query": "provider value"},
  215. source=EventSource.PROVIDER_RESOLVED,
  216. ),
  217. enabled_names=["example.lookup"],
  218. )
  219. assert result.status is EventStatus.SUCCESS
  220. assert result.arguments == {"query": "provider value"}
  221. assert resolver_calls == 0
  222. assert fallback_calls == 0
  223. @pytest.mark.asyncio
  224. async def test_provider_resolved_missing_arguments_return_invalid_without_fallback():
  225. fallback_calls = 0
  226. async def fallback(*args: Any) -> ResolvedEventArguments:
  227. nonlocal fallback_calls
  228. fallback_calls += 1
  229. return ResolvedEventArguments(
  230. event_name="example.lookup",
  231. arguments={"query": "fallback"},
  232. raw_arguments='{"query":"fallback"}',
  233. )
  234. result = await EventKernel(
  235. EventRegistry([_definition()]), argument_fallback=fallback
  236. ).execute(
  237. EventRequest(
  238. id="event-1",
  239. name="example.lookup",
  240. arguments={},
  241. source=EventSource.PROVIDER_RESOLVED,
  242. ),
  243. enabled_names=["example.lookup"],
  244. )
  245. assert result.status is EventStatus.INVALID_ARGUMENTS
  246. assert result.error == "missing required arguments: query"
  247. assert fallback_calls == 0
  248. @pytest.mark.asyncio
  249. @pytest.mark.parametrize(
  250. ("event_request", "enabled_names", "expected_status", "expected_error"),
  251. [
  252. (
  253. EventRequest(id="event-1", name="missing"),
  254. ["missing"],
  255. EventStatus.UNKNOWN,
  256. "unknown event",
  257. ),
  258. (
  259. EventRequest(id="event-1", name="example.lookup"),
  260. [],
  261. EventStatus.DISABLED,
  262. "event disabled",
  263. ),
  264. (
  265. EventRequest(
  266. id="event-1",
  267. name="example.lookup",
  268. arguments={"query": 42},
  269. source=EventSource.PROVIDER_RESOLVED,
  270. ),
  271. ["example.lookup"],
  272. EventStatus.INVALID_ARGUMENTS,
  273. "invalid argument type for query: expected string",
  274. ),
  275. ],
  276. )
  277. async def test_kernel_normalizes_lookup_and_validation_failures(
  278. event_request: EventRequest,
  279. enabled_names: list[str],
  280. expected_status: EventStatus,
  281. expected_error: str,
  282. ):
  283. result = await EventKernel(EventRegistry([_definition()])).execute(
  284. event_request,
  285. enabled_names=enabled_names,
  286. )
  287. assert result.status is expected_status
  288. assert result.error == expected_error
  289. @pytest.mark.asyncio
  290. async def test_kernel_normalizes_handler_exceptions():
  291. def fail(request: EventRequest) -> dict[str, Any]:
  292. raise RuntimeError("boom")
  293. result = await EventKernel(
  294. EventRegistry([_definition(handler=fail)])
  295. ).execute(
  296. EventRequest(
  297. id="event-1",
  298. name="example.lookup",
  299. arguments={"query": "value"},
  300. source=EventSource.PROVIDER_RESOLVED,
  301. ),
  302. enabled_names=["example.lookup"],
  303. )
  304. assert result.status is EventStatus.HANDLER_ERROR
  305. assert result.error == "event handler failed: boom"
  306. @pytest.mark.asyncio
  307. async def test_kernel_validates_complete_draft_2020_12_schema():
  308. definition = _definition(
  309. parameters={
  310. "type": "object",
  311. "properties": {
  312. "mode": {"enum": ["quick", "deep"]},
  313. "target": {"type": ["string", "null"]},
  314. "filters": {
  315. "type": "array",
  316. "items": {
  317. "type": "object",
  318. "properties": {"score": {"type": "number", "minimum": 0}},
  319. "required": ["score"],
  320. "additionalProperties": False,
  321. },
  322. },
  323. },
  324. "required": ["mode", "target", "filters"],
  325. "additionalProperties": False,
  326. },
  327. handler=lambda request: {"event": request.name},
  328. )
  329. kernel = EventKernel(EventRegistry([definition]))
  330. valid = await kernel.execute(
  331. EventRequest(
  332. id="valid",
  333. name=definition.name,
  334. arguments={
  335. "mode": "deep",
  336. "target": None,
  337. "filters": [{"score": 0.5}],
  338. },
  339. source=EventSource.PROVIDER_RESOLVED,
  340. ),
  341. enabled_names=[definition.name],
  342. )
  343. invalid = await kernel.execute(
  344. EventRequest(
  345. id="invalid",
  346. name=definition.name,
  347. arguments={
  348. "mode": "other",
  349. "target": 7,
  350. "filters": [{"score": -1, "extra": True}],
  351. "unexpected": True,
  352. },
  353. source=EventSource.PROVIDER_RESOLVED,
  354. ),
  355. enabled_names=[definition.name],
  356. )
  357. assert valid.status is EventStatus.SUCCESS
  358. assert invalid.status is EventStatus.INVALID_ARGUMENTS
  359. assert invalid.error.startswith("invalid event arguments:")
  360. @pytest.mark.asyncio
  361. async def test_kernel_normalizes_validator_runtime_exceptions():
  362. definition = _definition(parameters={"$ref": "urn:agent-lab:missing-schema"})
  363. result = await EventKernel(EventRegistry([definition])).execute(
  364. EventRequest(
  365. id="event-1",
  366. name=definition.name,
  367. arguments={},
  368. source=EventSource.PROVIDER_RESOLVED,
  369. ),
  370. enabled_names=[definition.name],
  371. )
  372. assert result.status is EventStatus.INVALID_ARGUMENTS
  373. assert result.error.startswith("event argument validation failed:")
  374. @pytest.mark.asyncio
  375. @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
  376. async def test_kernel_normalizes_resolution_boundary_exceptions(boundary: str):
  377. def resolver(request: EventRequest, context: EventExecutionContext) -> dict[str, Any]:
  378. if boundary == "resolver":
  379. raise RuntimeError("resolver boom")
  380. return {}
  381. async def fallback(*args: Any) -> ResolvedEventArguments:
  382. raise RuntimeError("fallback boom")
  383. result = await EventKernel(
  384. EventRegistry([_definition(resolver=resolver)]),
  385. argument_fallback=fallback,
  386. ).execute(
  387. EventRequest(id="event-1", name="example.lookup"),
  388. enabled_names=["example.lookup"],
  389. )
  390. assert result.status is EventStatus.RESOLUTION_ERROR
  391. assert result.error == f"event argument {boundary} failed: {boundary} boom"
  392. @pytest.mark.asyncio
  393. @pytest.mark.parametrize("boundary", ["resolver", "fallback"])
  394. async def test_kernel_normalizes_invalid_resolution_payloads(boundary: str):
  395. resolver = (
  396. (lambda request, context: None)
  397. if boundary == "resolver"
  398. else (lambda request, context: {})
  399. )
  400. async def fallback(*args: Any) -> Any:
  401. return {"query": "legacy bare mapping"}
  402. result = await EventKernel(
  403. EventRegistry([_definition(resolver=resolver)]),
  404. argument_fallback=fallback,
  405. ).execute(
  406. EventRequest(id="event-1", name="example.lookup"),
  407. enabled_names=["example.lookup"],
  408. )
  409. assert result.status is EventStatus.RESOLUTION_ERROR
  410. assert result.error == f"event argument {boundary} returned invalid payload"
  411. @pytest.mark.asyncio
  412. async def test_kernel_normalizes_non_json_resolver_arguments():
  413. result = await EventKernel(
  414. EventRegistry(
  415. [
  416. _definition(
  417. resolver=lambda request, context: {"query": object()}
  418. )
  419. ]
  420. )
  421. ).execute(
  422. EventRequest(id="event-1", name="example.lookup"),
  423. enabled_names=["example.lookup"],
  424. )
  425. assert result.status is EventStatus.RESOLUTION_ERROR
  426. assert result.error.startswith("event argument resolver failed to serialize:")
  427. @pytest.mark.asyncio
  428. async def test_kernel_normalizes_non_object_fallback_arguments():
  429. async def fallback(*args: Any) -> ResolvedEventArguments:
  430. return ResolvedEventArguments(
  431. event_name="example.lookup",
  432. arguments=["not", "an", "object"], # type: ignore[arg-type]
  433. raw_arguments='["not","an","object"]',
  434. )
  435. result = await EventKernel(
  436. EventRegistry([_definition(resolver=lambda request, context: {})]),
  437. argument_fallback=fallback,
  438. ).execute(
  439. EventRequest(id="event-1", name="example.lookup"),
  440. enabled_names=["example.lookup"],
  441. )
  442. assert result.status is EventStatus.RESOLUTION_ERROR
  443. assert result.error == "event argument fallback returned invalid payload"
  444. @pytest.mark.asyncio
  445. @pytest.mark.parametrize(
  446. ("resolved", "expected_error"),
  447. [
  448. (
  449. ResolvedEventArguments(
  450. event_name="another.event",
  451. arguments={"query": "value"},
  452. raw_arguments='{"query":"value"}',
  453. ),
  454. "fallback returned tool another.event for example.lookup",
  455. ),
  456. (
  457. ResolvedEventArguments(
  458. event_name="example.lookup",
  459. arguments={"query": "value"},
  460. raw_arguments="not-json",
  461. ),
  462. "fallback raw arguments are not valid JSON",
  463. ),
  464. (
  465. ResolvedEventArguments(
  466. event_name="example.lookup",
  467. arguments={"query": "parsed"},
  468. raw_arguments='{"query":"raw"}',
  469. ),
  470. "fallback raw arguments do not match parsed arguments",
  471. ),
  472. ],
  473. )
  474. async def test_kernel_rejects_inconsistent_structured_fallback(
  475. resolved: ResolvedEventArguments,
  476. expected_error: str,
  477. ):
  478. async def fallback(*args: Any) -> ResolvedEventArguments:
  479. return resolved
  480. result = await EventKernel(
  481. EventRegistry([_definition(resolver=lambda request, context: {})]),
  482. argument_fallback=fallback,
  483. ).execute(
  484. EventRequest(id="event-1", name="example.lookup"),
  485. enabled_names=["example.lookup"],
  486. )
  487. assert result.status is EventStatus.RESOLUTION_ERROR
  488. assert result.error == expected_error
  489. @pytest.mark.asyncio
  490. async def test_kernel_passes_consistent_fallback_arguments_and_raw_json_to_handler():
  491. captured: list[EventRequest] = []
  492. async def fallback(*args: Any) -> ResolvedEventArguments:
  493. return ResolvedEventArguments(
  494. event_name="example.lookup",
  495. arguments={"query": "resolved"},
  496. raw_arguments='{"query":"resolved"}',
  497. )
  498. result = await EventKernel(
  499. EventRegistry(
  500. [
  501. _definition(
  502. resolver=lambda request, context: {},
  503. handler=lambda request: captured.append(request) or {"ok": True},
  504. )
  505. ]
  506. ),
  507. argument_fallback=fallback,
  508. ).execute(
  509. EventRequest(id="event-1", name="example.lookup"),
  510. enabled_names=["example.lookup"],
  511. )
  512. assert result.status is EventStatus.SUCCESS
  513. assert captured[0].arguments == json.loads(captured[0].raw_arguments)
  514. assert result.raw_arguments == captured[0].raw_arguments
  515. @pytest.mark.asyncio
  516. @pytest.mark.parametrize("payload", [None, "text", 1, ["item"]])
  517. async def test_kernel_normalizes_invalid_handler_payloads(payload: Any):
  518. result = await EventKernel(
  519. EventRegistry([_definition(handler=lambda request: payload)])
  520. ).execute(
  521. EventRequest(
  522. id="event-1",
  523. name="example.lookup",
  524. arguments={"query": "value"},
  525. source=EventSource.PROVIDER_RESOLVED,
  526. ),
  527. enabled_names=["example.lookup"],
  528. )
  529. assert result.status is EventStatus.HANDLER_ERROR
  530. assert result.error == "event handler returned non-object payload"
  531. @pytest.mark.asyncio
  532. async def test_async_kernel_supports_async_handler():
  533. async def handler(request: EventRequest) -> dict[str, Any]:
  534. return {"query": request.arguments["query"]}
  535. result = await EventKernel(
  536. EventRegistry([_definition(handler=handler)])
  537. ).execute(
  538. EventRequest(
  539. id="event-1",
  540. name="example.lookup",
  541. arguments={"query": "async"},
  542. source=EventSource.PROVIDER_RESOLVED,
  543. ),
  544. enabled_names=["example.lookup"],
  545. )
  546. assert result.status is EventStatus.SUCCESS
  547. assert result.payload == {"query": "async"}
  548. @pytest.mark.asyncio
  549. async def test_tool_registry_async_entry_points_support_async_handler():
  550. async def handler(event: ToolCallEvent) -> dict[str, Any]:
  551. return {"tool": event.name, "query": event.arguments["query"]}
  552. registry = ToolRegistry(
  553. [
  554. ToolDefinition(
  555. name="async.lookup",
  556. description="Async lookup.",
  557. parameters={
  558. "type": "object",
  559. "properties": {"query": {"type": "string"}},
  560. "required": ["query"],
  561. },
  562. handler=handler,
  563. )
  564. ]
  565. )
  566. event = ToolCallEvent(
  567. id="call-1",
  568. name="async.lookup",
  569. arguments={"query": "value"},
  570. raw_arguments='{"query":"value"}',
  571. )
  572. assert await registry.handle_async(event) == {
  573. "tool": "async.lookup",
  574. "query": "value",
  575. }
  576. assert await registry.execute_async(event) == {
  577. "tool": "async.lookup",
  578. "query": "value",
  579. }
  580. def test_tool_registry_sync_facade_rejects_async_handler_without_runtime_warning():
  581. called = False
  582. async def handler(event: ToolCallEvent) -> dict[str, Any]:
  583. nonlocal called
  584. called = True
  585. return {"tool": event.name}
  586. registry = ToolRegistry(
  587. [
  588. ToolDefinition(
  589. name="async.lookup",
  590. description="Async lookup.",
  591. parameters={"type": "object"},
  592. handler=handler,
  593. )
  594. ]
  595. )
  596. event = ToolCallEvent(
  597. id="call-1",
  598. name="async.lookup",
  599. arguments={},
  600. raw_arguments="{}",
  601. )
  602. with warnings.catch_warnings(record=True) as captured:
  603. warnings.simplefilter("always")
  604. payload = registry.execute(event)
  605. gc.collect()
  606. assert payload == {
  607. "tool": "async.lookup",
  608. "error": "tool handler failed: async event handlers require execute_async",
  609. }
  610. assert called is False
  611. assert not [warning for warning in captured if issubclass(warning.category, RuntimeWarning)]
  612. @pytest.mark.asyncio
  613. async def test_kernel_rejects_boolean_for_json_number_arguments():
  614. definition = _definition(
  615. parameters={
  616. "type": "object",
  617. "properties": {"query": {"type": "number"}},
  618. "required": ["query"],
  619. }
  620. )
  621. result = await EventKernel(EventRegistry([definition])).execute(
  622. EventRequest(
  623. id="event-1",
  624. name="example.lookup",
  625. arguments={"query": True},
  626. source=EventSource.PROVIDER_RESOLVED,
  627. ),
  628. enabled_names=["example.lookup"],
  629. )
  630. assert result.status is EventStatus.INVALID_ARGUMENTS
  631. assert result.error == "invalid argument type for query: expected number"
  632. @pytest.mark.asyncio
  633. async def test_definition_metadata_survives_registration_and_result_creation():
  634. definition = _definition(
  635. result_policy=ResultPolicy.TEMPLATE_FOLLOW_UP,
  636. confirmation_policy=ConfirmationPolicy.REQUIRED,
  637. risk_level=RiskLevel.HIGH,
  638. idempotency_key_fields=("session_id", "event_id"),
  639. concurrency_class="device-write",
  640. conflict_keys=("device",),
  641. timeout_seconds=1.5,
  642. terminal=True,
  643. )
  644. registry = EventRegistry([definition])
  645. result = await EventKernel(registry).execute(
  646. EventRequest(
  647. id="event-1",
  648. name="example.lookup",
  649. arguments={"query": "value"},
  650. source=EventSource.PROVIDER_RESOLVED,
  651. ),
  652. enabled_names=["example.lookup"],
  653. )
  654. registered = registry.definition("example.lookup")
  655. assert registered is definition
  656. assert result.result_policy is ResultPolicy.TEMPLATE_FOLLOW_UP
  657. assert result.confirmation_policy is ConfirmationPolicy.REQUIRED
  658. assert result.risk_level is RiskLevel.HIGH
  659. assert result.idempotency_key_fields == ("session_id", "event_id")
  660. assert result.concurrency_class == "device-write"
  661. assert result.conflict_keys == ("device",)
  662. assert result.timeout_seconds == 1.5
  663. assert result.terminal is True
  664. @pytest.mark.asyncio
  665. @pytest.mark.parametrize("name", ["alpha.one", "beta-two", "任意.事件"])
  666. async def test_kernel_applies_identical_behavior_to_arbitrary_event_names(name: str):
  667. definition = _definition(name=name)
  668. result = await EventKernel(EventRegistry([definition])).execute(
  669. EventRequest(
  670. id="event-1",
  671. name=name,
  672. arguments={"query": "value"},
  673. source=EventSource.PROVIDER_RESOLVED,
  674. ),
  675. enabled_names=[name],
  676. )
  677. assert result.status is EventStatus.SUCCESS
  678. assert result.payload == {"event": name, "query": "value"}