@namzu/sdk 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/advisory/context.test.d.ts +16 -0
  3. package/dist/advisory/context.test.d.ts.map +1 -0
  4. package/dist/advisory/context.test.js +92 -0
  5. package/dist/advisory/context.test.js.map +1 -0
  6. package/dist/advisory/evaluator.test.d.ts +34 -0
  7. package/dist/advisory/evaluator.test.d.ts.map +1 -0
  8. package/dist/advisory/evaluator.test.js +172 -0
  9. package/dist/advisory/evaluator.test.js.map +1 -0
  10. package/dist/advisory/executor.test.d.ts +35 -0
  11. package/dist/advisory/executor.test.d.ts.map +1 -0
  12. package/dist/advisory/executor.test.js +233 -0
  13. package/dist/advisory/executor.test.js.map +1 -0
  14. package/dist/advisory/registry.test.d.ts +16 -0
  15. package/dist/advisory/registry.test.d.ts.map +1 -0
  16. package/dist/advisory/registry.test.js +62 -0
  17. package/dist/advisory/registry.test.js.map +1 -0
  18. package/dist/bridge/a2a/agent-card.test.d.ts +24 -0
  19. package/dist/bridge/a2a/agent-card.test.d.ts.map +1 -0
  20. package/dist/bridge/a2a/agent-card.test.js +118 -0
  21. package/dist/bridge/a2a/agent-card.test.js.map +1 -0
  22. package/dist/bridge/a2a/mapper.test.d.ts +29 -0
  23. package/dist/bridge/a2a/mapper.test.d.ts.map +1 -0
  24. package/dist/bridge/a2a/mapper.test.js +265 -0
  25. package/dist/bridge/a2a/mapper.test.js.map +1 -0
  26. package/dist/bridge/a2a/message.test.d.ts +20 -0
  27. package/dist/bridge/a2a/message.test.d.ts.map +1 -0
  28. package/dist/bridge/a2a/message.test.js +116 -0
  29. package/dist/bridge/a2a/message.test.js.map +1 -0
  30. package/dist/bridge/a2a/task.test.d.ts +29 -0
  31. package/dist/bridge/a2a/task.test.d.ts.map +1 -0
  32. package/dist/bridge/a2a/task.test.js +198 -0
  33. package/dist/bridge/a2a/task.test.js.map +1 -0
  34. package/dist/bridge/mcp/connector/adapter.test.d.ts +27 -0
  35. package/dist/bridge/mcp/connector/adapter.test.d.ts.map +1 -0
  36. package/dist/bridge/mcp/connector/adapter.test.js +203 -0
  37. package/dist/bridge/mcp/connector/adapter.test.js.map +1 -0
  38. package/dist/bridge/sse/mapper.test.d.ts +27 -0
  39. package/dist/bridge/sse/mapper.test.d.ts.map +1 -0
  40. package/dist/bridge/sse/mapper.test.js +271 -0
  41. package/dist/bridge/sse/mapper.test.js.map +1 -0
  42. package/dist/bridge/tools/connector/adapter.d.ts +2 -2
  43. package/dist/bridge/tools/connector/adapter.test.d.ts +28 -0
  44. package/dist/bridge/tools/connector/adapter.test.d.ts.map +1 -0
  45. package/dist/bridge/tools/connector/adapter.test.js +182 -0
  46. package/dist/bridge/tools/connector/adapter.test.js.map +1 -0
  47. package/dist/bridge/tools/connector/definitions.test.d.ts +23 -0
  48. package/dist/bridge/tools/connector/definitions.test.d.ts.map +1 -0
  49. package/dist/bridge/tools/connector/definitions.test.js +158 -0
  50. package/dist/bridge/tools/connector/definitions.test.js.map +1 -0
  51. package/dist/bridge/tools/connector/router.test.d.ts +21 -0
  52. package/dist/bridge/tools/connector/router.test.d.ts.map +1 -0
  53. package/dist/bridge/tools/connector/router.test.js +139 -0
  54. package/dist/bridge/tools/connector/router.test.js.map +1 -0
  55. package/dist/bus/breaker.test.d.ts +41 -0
  56. package/dist/bus/breaker.test.d.ts.map +1 -0
  57. package/dist/bus/breaker.test.js +242 -0
  58. package/dist/bus/breaker.test.js.map +1 -0
  59. package/dist/bus/index.d.ts +3 -1
  60. package/dist/bus/index.d.ts.map +1 -1
  61. package/dist/bus/index.js +18 -11
  62. package/dist/bus/index.js.map +1 -1
  63. package/dist/bus/index.test.d.ts +25 -0
  64. package/dist/bus/index.test.d.ts.map +1 -0
  65. package/dist/bus/index.test.js +151 -0
  66. package/dist/bus/index.test.js.map +1 -0
  67. package/dist/bus/lock.test.d.ts +44 -0
  68. package/dist/bus/lock.test.d.ts.map +1 -0
  69. package/dist/bus/lock.test.js +226 -0
  70. package/dist/bus/lock.test.js.map +1 -0
  71. package/dist/bus/ownership.test.d.ts +26 -0
  72. package/dist/bus/ownership.test.d.ts.map +1 -0
  73. package/dist/bus/ownership.test.js +205 -0
  74. package/dist/bus/ownership.test.js.map +1 -0
  75. package/dist/config/runtime.d.ts +28 -28
  76. package/dist/connector/BaseConnector.test.d.ts +21 -0
  77. package/dist/connector/BaseConnector.test.d.ts.map +1 -0
  78. package/dist/connector/BaseConnector.test.js +108 -0
  79. package/dist/connector/BaseConnector.test.js.map +1 -0
  80. package/dist/connector/builtins/http.test.d.ts +30 -0
  81. package/dist/connector/builtins/http.test.d.ts.map +1 -0
  82. package/dist/connector/builtins/http.test.js +232 -0
  83. package/dist/connector/builtins/http.test.js.map +1 -0
  84. package/dist/connector/builtins/webhook.test.d.ts +20 -0
  85. package/dist/connector/builtins/webhook.test.d.ts.map +1 -0
  86. package/dist/connector/builtins/webhook.test.js +113 -0
  87. package/dist/connector/builtins/webhook.test.js.map +1 -0
  88. package/dist/connector/execution/factory.test.d.ts +16 -0
  89. package/dist/connector/execution/factory.test.d.ts.map +1 -0
  90. package/dist/connector/execution/factory.test.js +64 -0
  91. package/dist/connector/execution/factory.test.js.map +1 -0
  92. package/dist/connector/execution/remote.test.d.ts +16 -0
  93. package/dist/connector/execution/remote.test.d.ts.map +1 -0
  94. package/dist/connector/execution/remote.test.js +53 -0
  95. package/dist/connector/execution/remote.test.js.map +1 -0
  96. package/dist/connector/mcp/adapter.test.d.ts +34 -0
  97. package/dist/connector/mcp/adapter.test.d.ts.map +1 -0
  98. package/dist/connector/mcp/adapter.test.js +199 -0
  99. package/dist/connector/mcp/adapter.test.js.map +1 -0
  100. package/dist/probe/context.d.ts +8 -0
  101. package/dist/probe/context.d.ts.map +1 -0
  102. package/dist/probe/context.js +7 -0
  103. package/dist/probe/context.js.map +1 -0
  104. package/dist/probe/errors.d.ts +12 -0
  105. package/dist/probe/errors.d.ts.map +1 -0
  106. package/dist/probe/errors.js +21 -0
  107. package/dist/probe/errors.js.map +1 -0
  108. package/dist/probe/index.d.ts +5 -0
  109. package/dist/probe/index.d.ts.map +1 -0
  110. package/dist/probe/index.js +4 -0
  111. package/dist/probe/index.js.map +1 -0
  112. package/dist/probe/registry.d.ts +24 -0
  113. package/dist/probe/registry.d.ts.map +1 -0
  114. package/dist/probe/registry.js +228 -0
  115. package/dist/probe/registry.js.map +1 -0
  116. package/dist/probe/registry.test.d.ts +7 -0
  117. package/dist/probe/registry.test.d.ts.map +1 -0
  118. package/dist/probe/registry.test.js +310 -0
  119. package/dist/probe/registry.test.js.map +1 -0
  120. package/dist/provider/instrumentation.d.ts +9 -0
  121. package/dist/provider/instrumentation.d.ts.map +1 -0
  122. package/dist/provider/instrumentation.js +104 -0
  123. package/dist/provider/instrumentation.js.map +1 -0
  124. package/dist/provider/instrumentation.test.d.ts +2 -0
  125. package/dist/provider/instrumentation.test.d.ts.map +1 -0
  126. package/dist/provider/instrumentation.test.js +152 -0
  127. package/dist/provider/instrumentation.test.js.map +1 -0
  128. package/dist/public-runtime.d.ts +5 -0
  129. package/dist/public-runtime.d.ts.map +1 -1
  130. package/dist/public-runtime.js +4 -0
  131. package/dist/public-runtime.js.map +1 -1
  132. package/dist/public-types.d.ts +3 -0
  133. package/dist/public-types.d.ts.map +1 -1
  134. package/dist/rag/chunking.test.d.ts +20 -0
  135. package/dist/rag/chunking.test.d.ts.map +1 -0
  136. package/dist/rag/chunking.test.js +92 -0
  137. package/dist/rag/chunking.test.js.map +1 -0
  138. package/dist/rag/context-assembler.test.d.ts +19 -0
  139. package/dist/rag/context-assembler.test.d.ts.map +1 -0
  140. package/dist/rag/context-assembler.test.js +98 -0
  141. package/dist/rag/context-assembler.test.js.map +1 -0
  142. package/dist/rag/embedding.test.d.ts +19 -0
  143. package/dist/rag/embedding.test.d.ts.map +1 -0
  144. package/dist/rag/embedding.test.js +115 -0
  145. package/dist/rag/embedding.test.js.map +1 -0
  146. package/dist/rag/ingestion.test.d.ts +22 -0
  147. package/dist/rag/ingestion.test.d.ts.map +1 -0
  148. package/dist/rag/ingestion.test.js +99 -0
  149. package/dist/rag/ingestion.test.js.map +1 -0
  150. package/dist/rag/knowledge-base.test.d.ts +17 -0
  151. package/dist/rag/knowledge-base.test.d.ts.map +1 -0
  152. package/dist/rag/knowledge-base.test.js +77 -0
  153. package/dist/rag/knowledge-base.test.js.map +1 -0
  154. package/dist/rag/rag-tool.test.d.ts +21 -0
  155. package/dist/rag/rag-tool.test.d.ts.map +1 -0
  156. package/dist/rag/rag-tool.test.js +149 -0
  157. package/dist/rag/rag-tool.test.js.map +1 -0
  158. package/dist/rag/retriever.test.d.ts +26 -0
  159. package/dist/rag/retriever.test.d.ts.map +1 -0
  160. package/dist/rag/retriever.test.js +180 -0
  161. package/dist/rag/retriever.test.js.map +1 -0
  162. package/dist/rag/vector-store.test.d.ts +38 -0
  163. package/dist/rag/vector-store.test.d.ts.map +1 -0
  164. package/dist/rag/vector-store.test.js +175 -0
  165. package/dist/rag/vector-store.test.js.map +1 -0
  166. package/dist/registry/ManagedRegistry.test.d.ts +21 -0
  167. package/dist/registry/ManagedRegistry.test.d.ts.map +1 -0
  168. package/dist/registry/ManagedRegistry.test.js +98 -0
  169. package/dist/registry/ManagedRegistry.test.js.map +1 -0
  170. package/dist/registry/Registry.test.d.ts +18 -0
  171. package/dist/registry/Registry.test.d.ts.map +1 -0
  172. package/dist/registry/Registry.test.js +79 -0
  173. package/dist/registry/Registry.test.js.map +1 -0
  174. package/dist/registry/agent/definitions.test.d.ts +15 -0
  175. package/dist/registry/agent/definitions.test.d.ts.map +1 -0
  176. package/dist/registry/agent/definitions.test.js +84 -0
  177. package/dist/registry/agent/definitions.test.js.map +1 -0
  178. package/dist/registry/connector/definitions.test.d.ts +13 -0
  179. package/dist/registry/connector/definitions.test.d.ts.map +1 -0
  180. package/dist/registry/connector/definitions.test.js +41 -0
  181. package/dist/registry/connector/definitions.test.js.map +1 -0
  182. package/dist/registry/connector/scoped.test.d.ts +21 -0
  183. package/dist/registry/connector/scoped.test.d.ts.map +1 -0
  184. package/dist/registry/connector/scoped.test.js +115 -0
  185. package/dist/registry/connector/scoped.test.js.map +1 -0
  186. package/dist/registry/plugin/index.test.d.ts +12 -0
  187. package/dist/registry/plugin/index.test.d.ts.map +1 -0
  188. package/dist/registry/plugin/index.test.js +69 -0
  189. package/dist/registry/plugin/index.test.js.map +1 -0
  190. package/dist/registry/tool/execute.test.d.ts +42 -0
  191. package/dist/registry/tool/execute.test.d.ts.map +1 -0
  192. package/dist/registry/tool/execute.test.js +281 -0
  193. package/dist/registry/tool/execute.test.js.map +1 -0
  194. package/dist/runtime/query/events.d.ts +3 -1
  195. package/dist/runtime/query/events.d.ts.map +1 -1
  196. package/dist/runtime/query/events.js +6 -1
  197. package/dist/runtime/query/events.js.map +1 -1
  198. package/dist/runtime/query/executor.d.ts +3 -1
  199. package/dist/runtime/query/executor.d.ts.map +1 -1
  200. package/dist/runtime/query/executor.js +30 -1
  201. package/dist/runtime/query/executor.js.map +1 -1
  202. package/dist/runtime/query/iteration/phases/advisory.test.d.ts +42 -0
  203. package/dist/runtime/query/iteration/phases/advisory.test.d.ts.map +1 -0
  204. package/dist/runtime/query/iteration/phases/advisory.test.js +334 -0
  205. package/dist/runtime/query/iteration/phases/advisory.test.js.map +1 -0
  206. package/dist/test-setup.d.ts +22 -0
  207. package/dist/test-setup.d.ts.map +1 -0
  208. package/dist/test-setup.js +23 -0
  209. package/dist/test-setup.js.map +1 -0
  210. package/dist/types/bus/index.d.ts +46 -2
  211. package/dist/types/bus/index.d.ts.map +1 -1
  212. package/dist/types/doctor/check.d.ts +41 -0
  213. package/dist/types/doctor/check.d.ts.map +1 -0
  214. package/dist/types/doctor/check.js +2 -0
  215. package/dist/types/doctor/check.js.map +1 -0
  216. package/dist/types/doctor/index.d.ts +2 -0
  217. package/dist/types/doctor/index.d.ts.map +1 -0
  218. package/dist/types/doctor/index.js +2 -0
  219. package/dist/types/doctor/index.js.map +1 -0
  220. package/dist/types/probe/event-kind.d.ts +6 -0
  221. package/dist/types/probe/event-kind.d.ts.map +1 -0
  222. package/dist/types/probe/event-kind.js +2 -0
  223. package/dist/types/probe/event-kind.js.map +1 -0
  224. package/dist/types/probe/event-of.d.ts +5 -0
  225. package/dist/types/probe/event-of.d.ts.map +1 -0
  226. package/dist/types/probe/event-of.js +2 -0
  227. package/dist/types/probe/event-of.js.map +1 -0
  228. package/dist/types/probe/index.d.ts +4 -0
  229. package/dist/types/probe/index.d.ts.map +1 -0
  230. package/dist/types/probe/index.js +2 -0
  231. package/dist/types/probe/index.js.map +1 -0
  232. package/dist/types/probe/registry.d.ts +27 -0
  233. package/dist/types/probe/registry.d.ts.map +1 -0
  234. package/dist/types/probe/registry.js +2 -0
  235. package/dist/types/probe/registry.js.map +1 -0
  236. package/dist/utils/logger.d.ts +1 -1
  237. package/dist/utils/logger.d.ts.map +1 -1
  238. package/dist/utils/logger.js +5 -0
  239. package/dist/utils/logger.js.map +1 -1
  240. package/dist/vault/instrumentation.d.ts +11 -0
  241. package/dist/vault/instrumentation.d.ts.map +1 -0
  242. package/dist/vault/instrumentation.js +32 -0
  243. package/dist/vault/instrumentation.js.map +1 -0
  244. package/dist/vault/instrumentation.test.d.ts +2 -0
  245. package/dist/vault/instrumentation.test.d.ts.map +1 -0
  246. package/dist/vault/instrumentation.test.js +80 -0
  247. package/dist/vault/instrumentation.test.js.map +1 -0
  248. package/package.json +4 -1
  249. package/src/advisory/context.test.ts +109 -0
  250. package/src/advisory/evaluator.test.ts +192 -0
  251. package/src/advisory/executor.test.ts +272 -0
  252. package/src/advisory/registry.test.ts +75 -0
  253. package/src/bridge/a2a/agent-card.test.ts +140 -0
  254. package/src/bridge/a2a/mapper.test.ts +293 -0
  255. package/src/bridge/a2a/message.test.ts +138 -0
  256. package/src/bridge/a2a/task.test.ts +235 -0
  257. package/src/bridge/mcp/connector/adapter.test.ts +230 -0
  258. package/src/bridge/sse/mapper.test.ts +422 -0
  259. package/src/bridge/tools/connector/adapter.test.ts +224 -0
  260. package/src/bridge/tools/connector/definitions.test.ts +183 -0
  261. package/src/bridge/tools/connector/router.test.ts +159 -0
  262. package/src/bus/breaker.test.ts +274 -0
  263. package/src/bus/index.test.ts +183 -0
  264. package/src/bus/index.ts +21 -10
  265. package/src/bus/lock.test.ts +265 -0
  266. package/src/bus/ownership.test.ts +243 -0
  267. package/src/connector/BaseConnector.test.ts +130 -0
  268. package/src/connector/builtins/http.test.ts +290 -0
  269. package/src/connector/builtins/webhook.test.ts +138 -0
  270. package/src/connector/execution/factory.test.ts +75 -0
  271. package/src/connector/execution/remote.test.ts +63 -0
  272. package/src/connector/mcp/adapter.test.ts +249 -0
  273. package/src/probe/context.ts +14 -0
  274. package/src/probe/errors.ts +27 -0
  275. package/src/probe/index.ts +4 -0
  276. package/src/probe/registry.test.ts +480 -0
  277. package/src/probe/registry.ts +276 -0
  278. package/src/provider/instrumentation.test.ts +192 -0
  279. package/src/provider/instrumentation.ts +139 -0
  280. package/src/public-runtime.ts +17 -0
  281. package/src/public-types.ts +3 -0
  282. package/src/rag/chunking.test.ts +107 -0
  283. package/src/rag/context-assembler.test.ts +114 -0
  284. package/src/rag/embedding.test.ts +130 -0
  285. package/src/rag/ingestion.test.ts +114 -0
  286. package/src/rag/knowledge-base.test.ts +106 -0
  287. package/src/rag/rag-tool.test.ts +167 -0
  288. package/src/rag/retriever.test.ts +210 -0
  289. package/src/rag/vector-store.test.ts +196 -0
  290. package/src/registry/ManagedRegistry.test.ts +118 -0
  291. package/src/registry/Registry.test.ts +91 -0
  292. package/src/registry/agent/definitions.test.ts +100 -0
  293. package/src/registry/connector/definitions.test.ts +51 -0
  294. package/src/registry/connector/scoped.test.ts +129 -0
  295. package/src/registry/plugin/index.test.ts +85 -0
  296. package/src/registry/tool/execute.test.ts +330 -0
  297. package/src/runtime/query/events.ts +6 -1
  298. package/src/runtime/query/executor.ts +34 -0
  299. package/src/runtime/query/iteration/phases/advisory.test.ts +412 -0
  300. package/src/test-setup.ts +24 -0
  301. package/src/types/bus/index.ts +54 -2
  302. package/src/types/doctor/check.ts +53 -0
  303. package/src/types/doctor/index.ts +9 -0
  304. package/src/types/probe/event-kind.ts +8 -0
  305. package/src/types/probe/event-of.ts +3 -0
  306. package/src/types/probe/index.ts +11 -0
  307. package/src/types/probe/registry.ts +36 -0
  308. package/src/utils/logger.ts +6 -1
  309. package/src/vault/instrumentation.test.ts +98 -0
  310. package/src/vault/instrumentation.ts +56 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
3
+ *
4
+ * - `canExecute(runId)` on an unknown runId returns true (no breaker
5
+ * means no constraint).
6
+ * - `recordFailure(runId)` lazily creates a breaker entry in `closed`
7
+ * state for unknown runIds; `recordSuccess(runId)` is a no-op when
8
+ * no entry exists.
9
+ * - Consecutive failures: after `failureThreshold` calls in a row
10
+ * without intervening success, `state` → `open`, `trippedAt` set,
11
+ * single `breaker_tripped` event.
12
+ * - In `open` state, `canExecute` returns false until `resetTimeoutMs`
13
+ * has elapsed since `trippedAt`; at that point it transitions to
14
+ * `half_open`, emits `breaker_half_open`, and returns true.
15
+ * - In `half_open`, `canExecute` keeps returning true (the probe is
16
+ * allowed more than once) until the next `recordSuccess` closes the
17
+ * breaker or the next `recordFailure` re-trips it.
18
+ * - `recordSuccess` in `half_open` → `closed`; emits
19
+ * `breaker_probe_success` then `breaker_reset` (two events, in
20
+ * that order).
21
+ * - `recordFailure` in `half_open` → `open`; emits
22
+ * `breaker_probe_failure` then `breaker_tripped` (two events).
23
+ * The `consecutiveFailures` count carries over (not reset by the
24
+ * half-open probe cycle).
25
+ * - `recordSuccess` in `closed` resets `consecutiveFailures` to 0 and
26
+ * updates `lastSuccessAt`; emits nothing.
27
+ * - `recordSuccess` while `open`: logs a warning, does NOT change
28
+ * state (behaviour is "discarded"); emits nothing.
29
+ * - `recordFailure` while `open`: no additional events; state stays
30
+ * open (consecutive counter does NOT advance from `recordFailure`
31
+ * while open in the current implementation either — see test).
32
+ * - `reset(runId)` forces the breaker to `closed` regardless of prior
33
+ * state; emits `breaker_reset`; clears `consecutiveFailures` and
34
+ * `trippedAt` but preserves `lastFailureAt` / `lastSuccessAt`.
35
+ * - `listTripped()` returns snapshots for every breaker in `open` or
36
+ * `half_open`; closed breakers are excluded.
37
+ * - Breaker entries are keyed per-`RunId`; state does not leak across
38
+ * runs. No per-tenant dimension (design.md §2.1 aspirational).
39
+ */
40
+
41
+ import fc from 'fast-check'
42
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
43
+
44
+ import type { AgentBusEvent } from '../types/bus/index.js'
45
+ import type { RunId } from '../types/ids/index.js'
46
+ import type { Logger } from '../utils/logger.js'
47
+
48
+ import { CircuitBreaker } from './breaker.js'
49
+
50
+ function makeLogger(): Logger {
51
+ const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
52
+ return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
53
+ }
54
+
55
+ function runId(n: number): RunId {
56
+ return `run_${n}` as RunId
57
+ }
58
+
59
+ const THRESHOLD = 5
60
+ const RESET_MS = 30_000
61
+
62
+ describe('CircuitBreaker', () => {
63
+ let events: AgentBusEvent[]
64
+ let breaker: CircuitBreaker
65
+
66
+ beforeEach(() => {
67
+ events = []
68
+ breaker = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
69
+ })
70
+
71
+ afterEach(() => {
72
+ vi.useRealTimers()
73
+ })
74
+
75
+ describe('canExecute', () => {
76
+ it('returns true for an unknown runId (no breaker entry yet)', () => {
77
+ expect(breaker.canExecute(runId(1))).toBe(true)
78
+ })
79
+ })
80
+
81
+ describe('recordFailure', () => {
82
+ it('lazily creates a closed breaker entry for unknown runIds', () => {
83
+ breaker.recordFailure(runId(1))
84
+ const snap = breaker.getSnapshot(runId(1))
85
+ expect(snap?.state).toBe('closed')
86
+ expect(snap?.consecutiveFailures).toBe(1)
87
+ })
88
+
89
+ it('trips after exactly `failureThreshold` consecutive failures', () => {
90
+ for (let i = 0; i < THRESHOLD - 1; i++) {
91
+ breaker.recordFailure(runId(1))
92
+ }
93
+ expect(breaker.getSnapshot(runId(1))?.state).toBe('closed')
94
+ expect(events.filter((e) => e.type === 'breaker_tripped')).toHaveLength(0)
95
+
96
+ breaker.recordFailure(runId(1))
97
+ expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
98
+ const trippedEvents = events.filter((e) => e.type === 'breaker_tripped')
99
+ expect(trippedEvents).toHaveLength(1)
100
+ if (trippedEvents[0]?.type === 'breaker_tripped') {
101
+ expect(trippedEvents[0].consecutiveFailures).toBe(THRESHOLD)
102
+ }
103
+ })
104
+
105
+ it('trips exactly once — further failures in `open` emit no new breaker_tripped', () => {
106
+ for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
107
+ events.length = 0
108
+
109
+ breaker.recordFailure(runId(1))
110
+ breaker.recordFailure(runId(1))
111
+ expect(events).toEqual([])
112
+ })
113
+
114
+ it('property: any N ≥ threshold consecutive failures trips exactly once', () => {
115
+ fc.assert(
116
+ fc.property(fc.integer({ min: THRESHOLD, max: THRESHOLD * 4 }), (n) => {
117
+ const local = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
118
+ for (let i = 0; i < n; i++) local.recordFailure(runId(999))
119
+ expect(local.getSnapshot(runId(999))?.state).toBe('open')
120
+ }),
121
+ { numRuns: 25 },
122
+ )
123
+ })
124
+
125
+ it('property: any N < threshold keeps the breaker closed', () => {
126
+ fc.assert(
127
+ fc.property(fc.integer({ min: 0, max: THRESHOLD - 1 }), (n) => {
128
+ const local = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
129
+ for (let i = 0; i < n; i++) local.recordFailure(runId(888))
130
+ const snap = local.getSnapshot(runId(888))
131
+ if (n === 0) {
132
+ expect(snap).toBeUndefined()
133
+ } else {
134
+ expect(snap?.state).toBe('closed')
135
+ }
136
+ }),
137
+ { numRuns: 25 },
138
+ )
139
+ })
140
+ })
141
+
142
+ describe('recordSuccess', () => {
143
+ it('is a no-op on an unknown runId — no breaker entry created', () => {
144
+ breaker.recordSuccess(runId(42))
145
+ expect(breaker.getSnapshot(runId(42))).toBeUndefined()
146
+ expect(events).toEqual([])
147
+ })
148
+
149
+ it('resets consecutiveFailures to 0 without changing closed state', () => {
150
+ breaker.recordFailure(runId(1))
151
+ breaker.recordFailure(runId(1))
152
+ breaker.recordSuccess(runId(1))
153
+ const snap = breaker.getSnapshot(runId(1))
154
+ expect(snap?.state).toBe('closed')
155
+ expect(snap?.consecutiveFailures).toBe(0)
156
+ expect(snap?.lastSuccessAt).toBeDefined()
157
+ })
158
+
159
+ it('is discarded (warned, no state change) when called while breaker is open', () => {
160
+ for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
161
+ events.length = 0
162
+
163
+ breaker.recordSuccess(runId(1))
164
+ expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
165
+ expect(events).toEqual([])
166
+ })
167
+ })
168
+
169
+ describe('open → half_open transition', () => {
170
+ it('transitions to half_open after resetTimeoutMs elapsed; emits breaker_half_open', () => {
171
+ vi.useFakeTimers()
172
+ const trip = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
173
+ for (let i = 0; i < THRESHOLD; i++) trip.recordFailure(runId(1))
174
+ events.length = 0
175
+
176
+ expect(trip.canExecute(runId(1))).toBe(false)
177
+
178
+ vi.advanceTimersByTime(RESET_MS - 1)
179
+ expect(trip.canExecute(runId(1))).toBe(false)
180
+
181
+ vi.advanceTimersByTime(1)
182
+ expect(trip.canExecute(runId(1))).toBe(true)
183
+ expect(trip.getSnapshot(runId(1))?.state).toBe('half_open')
184
+ expect(events).toEqual([{ type: 'breaker_half_open', agentRunId: runId(1) }])
185
+ })
186
+
187
+ it('canExecute in half_open keeps returning true until success or failure resolves', () => {
188
+ vi.useFakeTimers()
189
+ const trip = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
190
+ for (let i = 0; i < THRESHOLD; i++) trip.recordFailure(runId(1))
191
+ vi.advanceTimersByTime(RESET_MS)
192
+ trip.canExecute(runId(1)) // flip to half_open
193
+ expect(trip.canExecute(runId(1))).toBe(true)
194
+ expect(trip.canExecute(runId(1))).toBe(true)
195
+ })
196
+ })
197
+
198
+ describe('half_open → closed / open resolution', () => {
199
+ function setupHalfOpen(): CircuitBreaker {
200
+ vi.useFakeTimers()
201
+ const b = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
202
+ for (let i = 0; i < THRESHOLD; i++) b.recordFailure(runId(1))
203
+ vi.advanceTimersByTime(RESET_MS)
204
+ b.canExecute(runId(1)) // flip
205
+ events.length = 0
206
+ return b
207
+ }
208
+
209
+ it('recordSuccess in half_open closes the breaker + emits probe_success then reset', () => {
210
+ const b = setupHalfOpen()
211
+ b.recordSuccess(runId(1))
212
+ expect(b.getSnapshot(runId(1))?.state).toBe('closed')
213
+ expect(b.getSnapshot(runId(1))?.consecutiveFailures).toBe(0)
214
+ expect(events).toEqual([
215
+ { type: 'breaker_probe_success', agentRunId: runId(1) },
216
+ { type: 'breaker_reset', agentRunId: runId(1) },
217
+ ])
218
+ })
219
+
220
+ it('recordFailure in half_open re-trips + emits probe_failure then tripped', () => {
221
+ const b = setupHalfOpen()
222
+ b.recordFailure(runId(1))
223
+ expect(b.getSnapshot(runId(1))?.state).toBe('open')
224
+ expect(events).toEqual([
225
+ { type: 'breaker_probe_failure', agentRunId: runId(1) },
226
+ {
227
+ type: 'breaker_tripped',
228
+ agentRunId: runId(1),
229
+ consecutiveFailures: THRESHOLD + 1,
230
+ },
231
+ ])
232
+ })
233
+ })
234
+
235
+ describe('reset', () => {
236
+ it('forces a tripped breaker back to closed + emits breaker_reset', () => {
237
+ for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
238
+ events.length = 0
239
+
240
+ breaker.reset(runId(1))
241
+ const snap = breaker.getSnapshot(runId(1))
242
+ expect(snap?.state).toBe('closed')
243
+ expect(snap?.consecutiveFailures).toBe(0)
244
+ expect(snap?.trippedAt).toBeUndefined()
245
+ expect(events).toEqual([{ type: 'breaker_reset', agentRunId: runId(1) }])
246
+ })
247
+
248
+ it('is a no-op on an unknown runId (no event)', () => {
249
+ breaker.reset(runId(999))
250
+ expect(events).toEqual([])
251
+ })
252
+ })
253
+
254
+ describe('per-runId isolation', () => {
255
+ it('tripping one runId does not affect another', () => {
256
+ for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
257
+ expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
258
+ expect(breaker.canExecute(runId(2))).toBe(true)
259
+ expect(breaker.getSnapshot(runId(2))).toBeUndefined()
260
+ })
261
+ })
262
+
263
+ describe('listTripped', () => {
264
+ it('returns only open + half_open breakers; excludes closed', () => {
265
+ for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
266
+ breaker.recordFailure(runId(2))
267
+
268
+ const tripped = breaker.listTripped()
269
+ expect(tripped).toHaveLength(1)
270
+ expect(tripped[0]?.agentRunId).toBe(runId(1))
271
+ expect(tripped[0]?.state).toBe('open')
272
+ })
273
+ })
274
+ })
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
3
+ *
4
+ * - `AgentBus` composes a `FileLockManager`, `EditOwnershipTracker`,
5
+ * and `CircuitBreaker`; every event each of them emits is
6
+ * broadcast to bus-level listeners.
7
+ * - `on(listener)` returns an unsubscribe function; invoking it
8
+ * removes the listener, and subsequent events skip it.
9
+ * - Listeners receive events in the order they are emitted
10
+ * (preserved by `Set` insertion-order iteration). Multi-listener
11
+ * fan-out: every listener sees every event.
12
+ * - A throwing listener does NOT cascade — other listeners still
13
+ * receive the same event, and the bus logs the error via
14
+ * `log.error`.
15
+ * - No per-tenant routing — events are global to the bus instance
16
+ * (design.md §2.1 aspirational per-tenant ordering does not
17
+ * exist; see §2.7).
18
+ * - `cleanupAgent(runId)` releases every lock + every ownership +
19
+ * resets the breaker for that runId. Counts are logged; the
20
+ * method does not return them.
21
+ * - `maintenance()` expires stale locks via `locks.expireStale()`
22
+ * and logs the count when non-zero.
23
+ */
24
+
25
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
26
+
27
+ import type { AgentBusEvent } from '../types/bus/index.js'
28
+ import type { RunId } from '../types/ids/index.js'
29
+ import type { Logger } from '../utils/logger.js'
30
+
31
+ import { AgentBus } from './index.js'
32
+
33
+ function makeLogger(): Logger {
34
+ // Recursive self-returning child — AgentBus nests FileLockManager /
35
+ // EditOwnershipTracker / CircuitBreaker, each of which chains a
36
+ // `log.child(...)` call in its constructor.
37
+ const self = {
38
+ info: vi.fn(),
39
+ warn: vi.fn(),
40
+ error: vi.fn(),
41
+ debug: vi.fn(),
42
+ child: vi.fn(),
43
+ } as unknown as Logger
44
+ ;(self as { child: (ctx: unknown) => Logger }).child = vi.fn(() => self)
45
+ return self
46
+ }
47
+
48
+ function runId(n: number): RunId {
49
+ return `run_${n}` as RunId
50
+ }
51
+
52
+ describe('AgentBus', () => {
53
+ let bus: AgentBus
54
+ let log: Logger
55
+
56
+ beforeEach(() => {
57
+ log = makeLogger()
58
+ bus = new AgentBus(log, { lockTimeoutMs: 60_000, lockAcquireTimeoutMs: 60 })
59
+ })
60
+
61
+ afterEach(() => {
62
+ vi.useRealTimers()
63
+ })
64
+
65
+ describe('listener lifecycle', () => {
66
+ it('broadcasts events to every registered listener', async () => {
67
+ const a: AgentBusEvent[] = []
68
+ const b: AgentBusEvent[] = []
69
+ bus.on((e) => a.push(e))
70
+ bus.on((e) => b.push(e))
71
+
72
+ await bus.locks.acquire('/tmp/f.txt', runId(1))
73
+
74
+ expect(a.length).toBeGreaterThan(0)
75
+ expect(b).toEqual(a)
76
+ })
77
+
78
+ it('unsubscribe removes the listener; later events skip it', async () => {
79
+ const seen: AgentBusEvent[] = []
80
+ const off = bus.on((e) => seen.push(e))
81
+
82
+ await bus.locks.acquire('/tmp/a.txt', runId(1))
83
+ const countAfterFirst = seen.length
84
+ expect(countAfterFirst).toBeGreaterThan(0)
85
+
86
+ off()
87
+ await bus.locks.acquire('/tmp/b.txt', runId(1))
88
+ expect(seen.length).toBe(countAfterFirst)
89
+ })
90
+
91
+ it('preserves emission order per listener (Set insertion-order iteration)', async () => {
92
+ const seen: string[] = []
93
+ bus.on((e) => seen.push(e.type))
94
+
95
+ await bus.locks.acquire('/tmp/a.txt', runId(1))
96
+ bus.ownership.claim('/tmp/a.txt', runId(1))
97
+ bus.ownership.release('/tmp/a.txt', runId(1))
98
+ bus.locks.release('/tmp/a.txt', runId(1))
99
+
100
+ expect(seen).toEqual([
101
+ 'lock_acquired',
102
+ 'ownership_claimed',
103
+ 'ownership_released',
104
+ 'lock_released',
105
+ ])
106
+ })
107
+
108
+ it('a throwing listener does not cascade — other listeners still fire', async () => {
109
+ const good: AgentBusEvent[] = []
110
+ bus.on(() => {
111
+ throw new Error('boom')
112
+ })
113
+ bus.on((e) => good.push(e))
114
+
115
+ await bus.locks.acquire('/tmp/a.txt', runId(1))
116
+ expect(good.length).toBeGreaterThan(0)
117
+ })
118
+ })
119
+
120
+ describe('composed sub-components', () => {
121
+ it('CircuitBreaker events flow through the bus listener', () => {
122
+ const seen: AgentBusEvent[] = []
123
+ bus.on((e) => seen.push(e))
124
+
125
+ for (let i = 0; i < 5; i++) bus.breaker.recordFailure(runId(1))
126
+ expect(seen.filter((e) => e.type === 'breaker_tripped')).toHaveLength(1)
127
+ })
128
+
129
+ it('EditOwnershipTracker events flow through the bus listener', () => {
130
+ const seen: AgentBusEvent[] = []
131
+ bus.on((e) => seen.push(e))
132
+
133
+ bus.ownership.claim('/tmp/a.txt', runId(1))
134
+ bus.ownership.transfer('/tmp/a.txt', runId(1), runId(2))
135
+ bus.ownership.release('/tmp/a.txt', runId(2))
136
+
137
+ expect(seen.map((e) => e.type)).toEqual([
138
+ 'ownership_claimed',
139
+ 'ownership_transferred',
140
+ 'ownership_released',
141
+ ])
142
+ })
143
+ })
144
+
145
+ describe('cleanupAgent', () => {
146
+ it('releases locks + ownerships + resets breaker for the runId', async () => {
147
+ await bus.locks.acquire('/tmp/a.txt', runId(1))
148
+ bus.ownership.claim('/tmp/a.txt', runId(1))
149
+ for (let i = 0; i < 5; i++) bus.breaker.recordFailure(runId(1))
150
+ expect(bus.breaker.getSnapshot(runId(1))?.state).toBe('open')
151
+ expect(bus.locks.isLocked('/tmp/a.txt')).toBe(true)
152
+
153
+ bus.cleanupAgent(runId(1))
154
+
155
+ expect(bus.locks.isLocked('/tmp/a.txt')).toBe(false)
156
+ expect(bus.ownership.getOwner('/tmp/a.txt')).toBeUndefined()
157
+ expect(bus.breaker.getSnapshot(runId(1))?.state).toBe('closed')
158
+ })
159
+
160
+ it('does not affect other runIds', async () => {
161
+ await bus.locks.acquire('/tmp/a.txt', runId(1))
162
+ await bus.locks.acquire('/tmp/b.txt', runId(2))
163
+
164
+ bus.cleanupAgent(runId(1))
165
+
166
+ expect(bus.locks.isLocked('/tmp/a.txt')).toBe(false)
167
+ expect(bus.locks.isLocked('/tmp/b.txt')).toBe(true)
168
+ expect(bus.locks.getHolder('/tmp/b.txt')).toBe(runId(2))
169
+ })
170
+ })
171
+
172
+ describe('maintenance', () => {
173
+ it('sweeps expired locks', async () => {
174
+ vi.useFakeTimers()
175
+ const b = new AgentBus(makeLogger(), { lockTimeoutMs: 500, lockAcquireTimeoutMs: 30 })
176
+ await b.locks.acquire('/tmp/a.txt', runId(1))
177
+ vi.advanceTimersByTime(501)
178
+
179
+ b.maintenance()
180
+ expect(b.locks.isLocked('/tmp/a.txt')).toBe(false)
181
+ })
182
+ })
183
+ })
package/src/bus/index.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  DEFAULT_LOCK_TIMEOUT_MS,
6
6
  DEFAULT_MAX_LOCKS_PER_AGENT,
7
7
  } from '../constants/bus/index.js'
8
+ import { buildProbeContext } from '../probe/context.js'
9
+ import { type ProbeRegistry, probe as defaultProbeRegistry } from '../probe/registry.js'
8
10
  import type { AgentBusEvent, AgentBusEventListener } from '../types/bus/index.js'
9
11
  import type { RunId } from '../types/ids/index.js'
10
12
  import type { Logger } from '../utils/logger.js'
@@ -39,10 +41,17 @@ export class AgentBus {
39
41
  private readonly listeners: Set<AgentBusEventListener> = new Set()
40
42
  private readonly log: Logger
41
43
  private readonly config: AgentBusConfig
44
+ private readonly probes: ProbeRegistry
42
45
 
43
- constructor(log: Logger, config: Partial<AgentBusConfig> = {}) {
46
+ constructor(
47
+ log: Logger,
48
+ config: Partial<AgentBusConfig> = {},
49
+ probeRegistry: ProbeRegistry = defaultProbeRegistry,
50
+ ) {
44
51
  this.config = { ...DEFAULT_AGENT_BUS_CONFIG, ...config }
45
52
  this.log = log.child({ component: 'AgentBus' })
53
+ this.probes = probeRegistry
54
+ this.probes.setLogger(log)
46
55
 
47
56
  const emitFn = (event: AgentBusEvent): void => this.emit(event)
48
57
 
@@ -70,16 +79,18 @@ export class AgentBus {
70
79
  }
71
80
 
72
81
  private emit(event: AgentBusEvent): void {
73
- for (const listener of this.listeners) {
74
- try {
75
- listener(event)
76
- } catch (error) {
77
- this.log.error('event listener threw', {
78
- eventType: event.type,
79
- error: error instanceof Error ? error.message : String(error),
80
- })
82
+ this.probes.dispatch(event, buildProbeContext(), () => {
83
+ for (const listener of this.listeners) {
84
+ try {
85
+ listener(event)
86
+ } catch (error) {
87
+ this.log.error('event listener threw', {
88
+ eventType: event.type,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ })
91
+ }
81
92
  }
82
- }
93
+ })
83
94
  }
84
95
 
85
96
  cleanupAgent(runId: RunId): void {