@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,265 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
3
+ *
4
+ * - `acquire(path, owner)` returns immediately when the file is
5
+ * unlocked; creates a lock with `lockId = lock_<uuid>`,
6
+ * `expiresAt = now + lockTimeoutMs`, emits `lock_acquired`.
7
+ * - `acquire` on a file already held by the SAME owner is idempotent
8
+ * — returns `{acquired: true, lock}` (the existing lock) and emits
9
+ * nothing.
10
+ * - `acquire` on a file held by a DIFFERENT owner emits `lock_denied`
11
+ * and then polls at `LOCK_ACQUIRE_POLL_INTERVAL_MS` (100ms) intervals
12
+ * until either the holder releases, the lock expires, or
13
+ * `acquireTimeoutMs` elapses. No FIFO queue; retries race.
14
+ * - When the holder releases before the deadline, the waiting acquire
15
+ * succeeds on its next poll.
16
+ * - When `acquireTimeoutMs` elapses, acquire returns
17
+ * `{acquired: false, holder, filePath}` — holder is the current
18
+ * lock holder's owner if the lock still exists, else empty string.
19
+ * - `maxLocksPerAgent` blocks further acquisitions by the same owner.
20
+ * The internal `tryAcquire` returns `{holder: owner}` (a
21
+ * "yourself" sentinel) but the public `acquire()` swallows that and
22
+ * keeps polling for `acquireTimeoutMs` (there's no short-circuit
23
+ * for cap-exceeded). When the deadline expires and no lock exists
24
+ * on the file, the final `holder` is `''` (empty `RunId`) because
25
+ * the fallback reads `this.locks.get(filePath)?.owner ?? ''`.
26
+ * - `release(path, owner)` deletes the lock + emits `lock_released`
27
+ * when the current lock's owner matches; cleans up the per-owner
28
+ * lock set (and prunes the set entry when it becomes empty).
29
+ * Returns false without emitting when no lock or owner mismatch.
30
+ * - `releaseAll(owner)` drops every lock owned by `owner`, emits
31
+ * `lock_released` per lock, returns the count, and prunes the
32
+ * per-owner set.
33
+ * - `isLocked(path)` + `getHolder(path)` auto-expire any stale lock
34
+ * they observe (and emit `lock_expired`) before returning the
35
+ * current state.
36
+ * - `expireStale()` sweeps every lock; expired ones are deleted and
37
+ * emit `lock_expired`; returns the count.
38
+ * - Re-acquiring a path after its lock expires succeeds and assigns
39
+ * a FRESH `lockId` (the old id is never reused).
40
+ * - Lock state is per-`RunId`; no tenant dimension (design.md §2.1
41
+ * aspirational).
42
+ */
43
+
44
+ import { afterEach, describe, expect, it, vi } from 'vitest'
45
+
46
+ import type { AgentBusEvent } from '../types/bus/index.js'
47
+ import type { RunId } from '../types/ids/index.js'
48
+ import type { Logger } from '../utils/logger.js'
49
+
50
+ import { FileLockManager } from './lock.js'
51
+
52
+ function makeLogger(): Logger {
53
+ const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
54
+ return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
55
+ }
56
+
57
+ function runId(n: number): RunId {
58
+ return `run_${n}` as RunId
59
+ }
60
+
61
+ function makeManager(
62
+ overrides: Partial<{
63
+ lockTimeoutMs: number
64
+ acquireTimeoutMs: number
65
+ maxLocksPerAgent: number
66
+ }> = {},
67
+ ) {
68
+ const events: AgentBusEvent[] = []
69
+ const mgr = new FileLockManager(makeLogger(), (e) => events.push(e), {
70
+ // short defaults keep acquire-retry tests under a second total
71
+ lockTimeoutMs: 60_000,
72
+ acquireTimeoutMs: 200,
73
+ maxLocksPerAgent: 10,
74
+ ...overrides,
75
+ })
76
+ return { mgr, events }
77
+ }
78
+
79
+ describe('FileLockManager', () => {
80
+ afterEach(() => {
81
+ vi.useRealTimers()
82
+ })
83
+
84
+ describe('acquire (happy path)', () => {
85
+ it('acquires an unheld lock immediately + emits lock_acquired', async () => {
86
+ const { mgr, events } = makeManager()
87
+ const before = Date.now()
88
+ const result = await mgr.acquire('/tmp/a.txt', runId(1))
89
+ expect(result.acquired).toBe(true)
90
+ if (result.acquired) {
91
+ expect(result.lock.owner).toBe(runId(1))
92
+ expect(result.lock.filePath).toBe('/tmp/a.txt')
93
+ expect(result.lock.lockId).toMatch(/^lock_[0-9a-f-]+$/)
94
+ expect(result.lock.acquiredAt).toBeGreaterThanOrEqual(before)
95
+ expect(result.lock.expiresAt).toBeGreaterThan(result.lock.acquiredAt)
96
+ }
97
+ const acquired = events.filter((e) => e.type === 'lock_acquired')
98
+ expect(acquired).toHaveLength(1)
99
+ })
100
+
101
+ it('is idempotent for the same owner — returns the existing lock, emits nothing', async () => {
102
+ const { mgr, events } = makeManager()
103
+ const first = await mgr.acquire('/tmp/a.txt', runId(1))
104
+ events.length = 0
105
+ const second = await mgr.acquire('/tmp/a.txt', runId(1))
106
+ expect(second.acquired).toBe(true)
107
+ if (first.acquired && second.acquired) {
108
+ expect(second.lock.lockId).toBe(first.lock.lockId)
109
+ }
110
+ expect(events).toEqual([])
111
+ })
112
+ })
113
+
114
+ describe('acquire (contention)', () => {
115
+ it('emits lock_denied when another owner holds the lock and no release happens', async () => {
116
+ const { mgr, events } = makeManager({ acquireTimeoutMs: 120 })
117
+ await mgr.acquire('/tmp/a.txt', runId(1))
118
+ events.length = 0
119
+
120
+ const result = await mgr.acquire('/tmp/a.txt', runId(2))
121
+ expect(result.acquired).toBe(false)
122
+ if (!result.acquired) {
123
+ expect(result.holder).toBe(runId(1))
124
+ expect(result.filePath).toBe('/tmp/a.txt')
125
+ }
126
+ expect(events.some((e) => e.type === 'lock_denied')).toBe(true)
127
+ })
128
+
129
+ it('succeeds on a retry once the holder releases before the deadline', async () => {
130
+ const { mgr } = makeManager({ acquireTimeoutMs: 500 })
131
+ await mgr.acquire('/tmp/a.txt', runId(1))
132
+
133
+ const contender = mgr.acquire('/tmp/a.txt', runId(2))
134
+ setTimeout(() => mgr.release('/tmp/a.txt', runId(1)), 120)
135
+
136
+ const result = await contender
137
+ expect(result.acquired).toBe(true)
138
+ if (result.acquired) expect(result.lock.owner).toBe(runId(2))
139
+ })
140
+ })
141
+
142
+ describe('maxLocksPerAgent cap', () => {
143
+ it('denies a new acquisition when the owner is at cap — acquire polls to deadline then returns empty holder', async () => {
144
+ const { mgr } = makeManager({ maxLocksPerAgent: 2, acquireTimeoutMs: 60 })
145
+ await mgr.acquire('/tmp/a.txt', runId(1))
146
+ await mgr.acquire('/tmp/b.txt', runId(1))
147
+
148
+ const over = await mgr.acquire('/tmp/c.txt', runId(1))
149
+ expect(over.acquired).toBe(false)
150
+ if (!over.acquired) {
151
+ // No lock exists on /tmp/c.txt, so the fallback holder is ''.
152
+ expect(over.holder).toBe('' as RunId)
153
+ expect(over.filePath).toBe('/tmp/c.txt')
154
+ }
155
+ })
156
+ })
157
+
158
+ describe('release', () => {
159
+ it('releases an owned lock + emits lock_released', async () => {
160
+ const { mgr, events } = makeManager()
161
+ await mgr.acquire('/tmp/a.txt', runId(1))
162
+ events.length = 0
163
+
164
+ expect(mgr.release('/tmp/a.txt', runId(1))).toBe(true)
165
+ expect(mgr.isLocked('/tmp/a.txt')).toBe(false)
166
+ expect(events.some((e) => e.type === 'lock_released')).toBe(true)
167
+ })
168
+
169
+ it('returns false + emits nothing when the caller is not the holder', async () => {
170
+ const { mgr, events } = makeManager()
171
+ await mgr.acquire('/tmp/a.txt', runId(1))
172
+ events.length = 0
173
+
174
+ expect(mgr.release('/tmp/a.txt', runId(2))).toBe(false)
175
+ expect(mgr.isLocked('/tmp/a.txt')).toBe(true)
176
+ expect(events).toEqual([])
177
+ })
178
+
179
+ it('returns false when no lock exists', () => {
180
+ const { mgr, events } = makeManager()
181
+ expect(mgr.release('/tmp/never.txt', runId(1))).toBe(false)
182
+ expect(events).toEqual([])
183
+ })
184
+ })
185
+
186
+ describe('releaseAll', () => {
187
+ it('drops every lock owned by the runId, emits one event per lock, returns count', async () => {
188
+ const { mgr, events } = makeManager()
189
+ await mgr.acquire('/tmp/a.txt', runId(1))
190
+ await mgr.acquire('/tmp/b.txt', runId(1))
191
+ await mgr.acquire('/tmp/c.txt', runId(2))
192
+ events.length = 0
193
+
194
+ const count = mgr.releaseAll(runId(1))
195
+ expect(count).toBe(2)
196
+ expect(mgr.isLocked('/tmp/a.txt')).toBe(false)
197
+ expect(mgr.isLocked('/tmp/b.txt')).toBe(false)
198
+ expect(mgr.isLocked('/tmp/c.txt')).toBe(true)
199
+ expect(events.filter((e) => e.type === 'lock_released')).toHaveLength(2)
200
+ })
201
+
202
+ it('returns 0 when the owner has no locks', () => {
203
+ const { mgr } = makeManager()
204
+ expect(mgr.releaseAll(runId(99))).toBe(0)
205
+ })
206
+ })
207
+
208
+ describe('expiry', () => {
209
+ it('expireStale drops expired locks + emits lock_expired per drop', async () => {
210
+ vi.useFakeTimers()
211
+ const { mgr, events } = makeManager({ lockTimeoutMs: 10_000 })
212
+ await mgr.acquire('/tmp/a.txt', runId(1))
213
+ await mgr.acquire('/tmp/b.txt', runId(2))
214
+ events.length = 0
215
+
216
+ vi.advanceTimersByTime(10_001)
217
+ const expired = mgr.expireStale()
218
+ expect(expired).toBe(2)
219
+ expect(events.filter((e) => e.type === 'lock_expired')).toHaveLength(2)
220
+ })
221
+
222
+ it('isLocked / getHolder auto-expire a stale lock before answering', async () => {
223
+ vi.useFakeTimers()
224
+ const { mgr, events } = makeManager({ lockTimeoutMs: 5_000 })
225
+ await mgr.acquire('/tmp/a.txt', runId(1))
226
+
227
+ vi.advanceTimersByTime(5_001)
228
+ events.length = 0
229
+ expect(mgr.isLocked('/tmp/a.txt')).toBe(false)
230
+ expect(events.some((e) => e.type === 'lock_expired')).toBe(true)
231
+
232
+ events.length = 0
233
+ expect(mgr.getHolder('/tmp/a.txt')).toBeUndefined()
234
+ // already expired by the previous call; no second emit
235
+ expect(events).toEqual([])
236
+ })
237
+
238
+ it('a fresh acquire after expiry assigns a new lockId', async () => {
239
+ vi.useFakeTimers()
240
+ const { mgr } = makeManager({ lockTimeoutMs: 1_000 })
241
+ const first = await mgr.acquire('/tmp/a.txt', runId(1))
242
+ vi.advanceTimersByTime(1_001)
243
+ mgr.expireStale()
244
+
245
+ vi.useRealTimers()
246
+ const second = await mgr.acquire('/tmp/a.txt', runId(2))
247
+ expect(first.acquired && second.acquired).toBe(true)
248
+ if (first.acquired && second.acquired) {
249
+ expect(second.lock.lockId).not.toBe(first.lock.lockId)
250
+ expect(second.lock.owner).toBe(runId(2))
251
+ }
252
+ })
253
+ })
254
+
255
+ describe('per-runId isolation', () => {
256
+ it('different runIds can hold locks on different files concurrently', async () => {
257
+ const { mgr } = makeManager()
258
+ const a = await mgr.acquire('/tmp/a.txt', runId(1))
259
+ const b = await mgr.acquire('/tmp/b.txt', runId(2))
260
+ expect(a.acquired && b.acquired).toBe(true)
261
+ expect(mgr.getHolder('/tmp/a.txt')).toBe(runId(1))
262
+ expect(mgr.getHolder('/tmp/b.txt')).toBe(runId(2))
263
+ })
264
+ })
265
+ })
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
3
+ *
4
+ * - `claim(path, owner)` on an unowned file creates ownership, emits
5
+ * `ownership_claimed`, returns `{claimed: true, ownership}`.
6
+ * - `claim(path, owner)` by the same owner is idempotent — returns
7
+ * `{claimed: true, ownership}` WITHOUT re-emitting.
8
+ * - `claim(path, owner)` by a different owner is denied — emits
9
+ * `ownership_denied`, returns `{claimed: false, currentOwner, filePath}`.
10
+ * - `release(path, owner)` on the current owner deletes + emits; returns
11
+ * true. On mismatch or missing entry: returns false; no emit.
12
+ * - `transfer(path, from, to)` requires the current owner to equal
13
+ * `from`. Success replaces the entry with a new `claimedAt`, emits
14
+ * `ownership_transferred`; no intervening `ownership_released` or
15
+ * `ownership_claimed` events. Failure returns false; no emit.
16
+ * - `releaseAll(owner)` sweeps every ownership for `owner`, emits
17
+ * `ownership_released` per entry, returns the count. Other owners'
18
+ * entries are untouched.
19
+ * - File paths are normalised via `path.resolve` before keying —
20
+ * `./foo/bar` and the absolute resolution of the same path collide
21
+ * into one ownership slot.
22
+ * - Ownership is not per-tenant; only per-`RunId`. There is no tenant
23
+ * isolation at this layer (design.md §2.1 aspirational; see §2.7).
24
+ */
25
+
26
+ import path from 'node:path'
27
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
28
+
29
+ import type { AgentBusEvent } from '../types/bus/index.js'
30
+ import type { RunId } from '../types/ids/index.js'
31
+ import type { Logger } from '../utils/logger.js'
32
+
33
+ import { EditOwnershipTracker } from './ownership.js'
34
+
35
+ function makeLogger(): Logger {
36
+ const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
37
+ return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
38
+ }
39
+
40
+ function runId(n: number): RunId {
41
+ return `run_${n}` as RunId
42
+ }
43
+
44
+ describe('EditOwnershipTracker', () => {
45
+ let events: AgentBusEvent[]
46
+ let tracker: EditOwnershipTracker
47
+
48
+ beforeEach(() => {
49
+ events = []
50
+ tracker = new EditOwnershipTracker(makeLogger(), (e) => events.push(e))
51
+ })
52
+
53
+ describe('claim', () => {
54
+ it('claims an unowned file, emits ownership_claimed, returns ownership', () => {
55
+ const before = Date.now()
56
+ const result = tracker.claim('/tmp/file.txt', runId(1))
57
+ const after = Date.now()
58
+
59
+ expect(result.claimed).toBe(true)
60
+ if (result.claimed) {
61
+ expect(result.ownership.owner).toBe(runId(1))
62
+ expect(result.ownership.filePath).toBe(path.resolve('/tmp/file.txt'))
63
+ expect(result.ownership.claimedAt).toBeGreaterThanOrEqual(before)
64
+ expect(result.ownership.claimedAt).toBeLessThanOrEqual(after)
65
+ }
66
+ expect(events).toEqual([
67
+ { type: 'ownership_claimed', filePath: path.resolve('/tmp/file.txt'), owner: runId(1) },
68
+ ])
69
+ })
70
+
71
+ it('is idempotent when the same owner re-claims — no re-emit', () => {
72
+ tracker.claim('/tmp/file.txt', runId(1))
73
+ events.length = 0
74
+
75
+ const result = tracker.claim('/tmp/file.txt', runId(1))
76
+ expect(result.claimed).toBe(true)
77
+ expect(events).toEqual([])
78
+ })
79
+
80
+ it('denies a claim by a different owner, emits ownership_denied', () => {
81
+ tracker.claim('/tmp/file.txt', runId(1))
82
+ events.length = 0
83
+
84
+ const result = tracker.claim('/tmp/file.txt', runId(2))
85
+ expect(result.claimed).toBe(false)
86
+ if (!result.claimed) {
87
+ expect(result.currentOwner).toBe(runId(1))
88
+ expect(result.filePath).toBe(path.resolve('/tmp/file.txt'))
89
+ }
90
+ expect(events).toEqual([
91
+ {
92
+ type: 'ownership_denied',
93
+ filePath: path.resolve('/tmp/file.txt'),
94
+ requester: runId(2),
95
+ currentOwner: runId(1),
96
+ },
97
+ ])
98
+ })
99
+
100
+ it('normalises file paths — equivalent paths collide', () => {
101
+ tracker.claim('/tmp/foo/../file.txt', runId(1))
102
+ const result = tracker.claim('/tmp/file.txt', runId(2))
103
+ expect(result.claimed).toBe(false)
104
+ })
105
+ })
106
+
107
+ describe('release', () => {
108
+ it('releases an owned file, emits ownership_released, returns true', () => {
109
+ tracker.claim('/tmp/file.txt', runId(1))
110
+ events.length = 0
111
+
112
+ const ok = tracker.release('/tmp/file.txt', runId(1))
113
+ expect(ok).toBe(true)
114
+ expect(tracker.getOwner('/tmp/file.txt')).toBeUndefined()
115
+ expect(events).toEqual([
116
+ {
117
+ type: 'ownership_released',
118
+ filePath: path.resolve('/tmp/file.txt'),
119
+ previousOwner: runId(1),
120
+ },
121
+ ])
122
+ })
123
+
124
+ it('returns false when no ownership exists — no emit', () => {
125
+ const ok = tracker.release('/tmp/never-claimed.txt', runId(1))
126
+ expect(ok).toBe(false)
127
+ expect(events).toEqual([])
128
+ })
129
+
130
+ it('returns false when owner mismatches — no emit', () => {
131
+ tracker.claim('/tmp/file.txt', runId(1))
132
+ events.length = 0
133
+
134
+ const ok = tracker.release('/tmp/file.txt', runId(2))
135
+ expect(ok).toBe(false)
136
+ expect(tracker.getOwner('/tmp/file.txt')).toBe(runId(1))
137
+ expect(events).toEqual([])
138
+ })
139
+ })
140
+
141
+ describe('transfer', () => {
142
+ it('transfers ownership from current owner to another — atomic, single event', () => {
143
+ tracker.claim('/tmp/file.txt', runId(1))
144
+ events.length = 0
145
+
146
+ const ok = tracker.transfer('/tmp/file.txt', runId(1), runId(2))
147
+ expect(ok).toBe(true)
148
+ expect(tracker.getOwner('/tmp/file.txt')).toBe(runId(2))
149
+ expect(events).toEqual([
150
+ {
151
+ type: 'ownership_transferred',
152
+ filePath: path.resolve('/tmp/file.txt'),
153
+ from: runId(1),
154
+ to: runId(2),
155
+ },
156
+ ])
157
+ })
158
+
159
+ it('returns false when the `from` argument is not the current owner — no emit', () => {
160
+ tracker.claim('/tmp/file.txt', runId(1))
161
+ events.length = 0
162
+
163
+ const ok = tracker.transfer('/tmp/file.txt', runId(99), runId(2))
164
+ expect(ok).toBe(false)
165
+ expect(tracker.getOwner('/tmp/file.txt')).toBe(runId(1))
166
+ expect(events).toEqual([])
167
+ })
168
+
169
+ it('returns false when no ownership exists — no emit', () => {
170
+ const ok = tracker.transfer('/tmp/file.txt', runId(1), runId(2))
171
+ expect(ok).toBe(false)
172
+ expect(events).toEqual([])
173
+ })
174
+
175
+ it('refreshes claimedAt on successful transfer', async () => {
176
+ tracker.claim('/tmp/file.txt', runId(1))
177
+ const t0 = tracker.getOwner('/tmp/file.txt')
178
+ expect(t0).toBe(runId(1))
179
+
180
+ await new Promise((r) => setTimeout(r, 2))
181
+ tracker.transfer('/tmp/file.txt', runId(1), runId(2))
182
+ const list = tracker.listByOwner(runId(2))
183
+ expect(list).toHaveLength(1)
184
+ const after = list[0]
185
+ expect(after?.claimedAt).toBeGreaterThan(0)
186
+ expect(after?.owner).toBe(runId(2))
187
+ })
188
+ })
189
+
190
+ describe('releaseAll', () => {
191
+ it('releases every ownership for the owner, returns count, emits per-entry', () => {
192
+ tracker.claim('/tmp/a.txt', runId(1))
193
+ tracker.claim('/tmp/b.txt', runId(1))
194
+ tracker.claim('/tmp/c.txt', runId(2))
195
+ events.length = 0
196
+
197
+ const count = tracker.releaseAll(runId(1))
198
+ expect(count).toBe(2)
199
+ expect(tracker.getOwner('/tmp/a.txt')).toBeUndefined()
200
+ expect(tracker.getOwner('/tmp/b.txt')).toBeUndefined()
201
+ expect(tracker.getOwner('/tmp/c.txt')).toBe(runId(2))
202
+
203
+ const released = events.filter((e) => e.type === 'ownership_released')
204
+ expect(released.length).toBe(2)
205
+ for (const e of released) {
206
+ if (e.type === 'ownership_released') {
207
+ expect(e.previousOwner).toBe(runId(1))
208
+ }
209
+ }
210
+ })
211
+
212
+ it('returns 0 when the owner has no entries', () => {
213
+ const count = tracker.releaseAll(runId(99))
214
+ expect(count).toBe(0)
215
+ expect(events).toEqual([])
216
+ })
217
+ })
218
+
219
+ describe('read helpers', () => {
220
+ it('getOwner returns undefined for unclaimed paths', () => {
221
+ expect(tracker.getOwner('/tmp/anything.txt')).toBeUndefined()
222
+ })
223
+
224
+ it('listByOwner returns all ownerships for a given owner', () => {
225
+ tracker.claim('/tmp/a.txt', runId(1))
226
+ tracker.claim('/tmp/b.txt', runId(1))
227
+ tracker.claim('/tmp/c.txt', runId(2))
228
+
229
+ const list = tracker.listByOwner(runId(1))
230
+ expect(list.length).toBe(2)
231
+ expect(new Set(list.map((o) => o.filePath))).toEqual(
232
+ new Set([path.resolve('/tmp/a.txt'), path.resolve('/tmp/b.txt')]),
233
+ )
234
+ })
235
+
236
+ it('checkConflict returns the current owner iff a different owner holds the file', () => {
237
+ tracker.claim('/tmp/file.txt', runId(1))
238
+ expect(tracker.checkConflict('/tmp/file.txt', runId(1))).toBeUndefined()
239
+ expect(tracker.checkConflict('/tmp/file.txt', runId(2))).toBe(runId(1))
240
+ expect(tracker.checkConflict('/tmp/other.txt', runId(2))).toBeUndefined()
241
+ })
242
+ })
243
+ })
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 5):
3
+ *
4
+ * - `BaseConnector` is abstract. Each concrete subclass sets id /
5
+ * name / description / connectionType / configSchema / methods
6
+ * and implements connect / disconnect / healthCheck / execute.
7
+ * - `toDefinition()` projects the abstract readonly fields into a
8
+ * `ConnectorDefinition<TConfig>` — used to register the connector
9
+ * with the `ConnectorRegistry`.
10
+ * - `findMethod(name)` returns the method by name or undefined.
11
+ * - `requireMethod(name)` throws with the method name + available
12
+ * names when not found.
13
+ * - `validateInput(method, input)`: zod.safeParse; on failure
14
+ * throws `Invalid input for method "<name>": <joined issues>`.
15
+ * - `measureExecution(fn)` returns `{result, durationMs}` with
16
+ * durationMs rounded.
17
+ * - Auth resolution lives in subclasses (see HttpConnector tests),
18
+ * not in the base class.
19
+ */
20
+
21
+ import { describe, expect, it } from 'vitest'
22
+ import { z } from 'zod'
23
+
24
+ import type {
25
+ AuthConfig,
26
+ ConnectionType,
27
+ ConnectorExecuteResult,
28
+ ConnectorMethod,
29
+ } from '../types/connector/index.js'
30
+ import type { ConnectorId } from '../types/ids/index.js'
31
+
32
+ import { BaseConnector } from './BaseConnector.js'
33
+
34
+ class TestConnector extends BaseConnector<{ base: string }> {
35
+ readonly id = 'conn_test' as ConnectorId
36
+ readonly name = 'Test'
37
+ readonly description = 'Test connector'
38
+ readonly connectionType: ConnectionType = 'custom'
39
+ readonly configSchema = z.object({ base: z.string() })
40
+ readonly methods: ConnectorMethod[] = [
41
+ {
42
+ name: 'echo',
43
+ description: 'echo',
44
+ inputSchema: z.object({ value: z.string() }),
45
+ },
46
+ ]
47
+
48
+ async connect(): Promise<void> {}
49
+ async disconnect(): Promise<void> {}
50
+ async healthCheck(): Promise<boolean> {
51
+ return true
52
+ }
53
+ async execute(): Promise<ConnectorExecuteResult> {
54
+ return { success: true, output: 'ok', durationMs: 0 }
55
+ }
56
+
57
+ // Expose protected methods for direct tests:
58
+ publicRequireMethod(name: string): ConnectorMethod {
59
+ return this.requireMethod(name)
60
+ }
61
+ publicValidateInput(method: ConnectorMethod, input: unknown): unknown {
62
+ return this.validateInput(method, input)
63
+ }
64
+ publicMeasureExecution<T>(fn: () => Promise<T>) {
65
+ return this.measureExecution(fn)
66
+ }
67
+
68
+ // Exposed internal auth resolver on a subclass is a sibling concern —
69
+ // `BaseConnector` does not define any auth handling; the field is
70
+ // just stored.
71
+ getStoredAuth(): AuthConfig | undefined {
72
+ return this.auth
73
+ }
74
+ }
75
+
76
+ describe('BaseConnector', () => {
77
+ it('toDefinition projects abstract readonly fields', () => {
78
+ const c = new TestConnector()
79
+ const def = c.toDefinition()
80
+ expect(def.id).toBe('conn_test')
81
+ expect(def.name).toBe('Test')
82
+ expect(def.description).toBe('Test connector')
83
+ expect(def.connectionType).toBe('custom')
84
+ expect(def.methods).toHaveLength(1)
85
+ })
86
+
87
+ it('findMethod returns undefined for an unknown name', () => {
88
+ const c = new TestConnector()
89
+ expect(c.publicRequireMethod).toBeDefined()
90
+ // findMethod is invoked indirectly via requireMethod — we cover the
91
+ // positive + negative paths below.
92
+ })
93
+
94
+ it('requireMethod returns the method when present', () => {
95
+ const c = new TestConnector()
96
+ const method = c.publicRequireMethod('echo')
97
+ expect(method.name).toBe('echo')
98
+ })
99
+
100
+ it('requireMethod throws naming the missing method + available names', () => {
101
+ const c = new TestConnector()
102
+ expect(() => c.publicRequireMethod('nope')).toThrow(/Method "nope" not found/)
103
+ expect(() => c.publicRequireMethod('nope')).toThrow(/Available: echo/)
104
+ })
105
+
106
+ it('validateInput passes through parsed data on success', () => {
107
+ const c = new TestConnector()
108
+ const method = c.publicRequireMethod('echo')
109
+ expect(c.publicValidateInput(method, { value: 'hi' })).toEqual({ value: 'hi' })
110
+ })
111
+
112
+ it('validateInput throws with joined issue messages on failure', () => {
113
+ const c = new TestConnector()
114
+ const method = c.publicRequireMethod('echo')
115
+ expect(() => c.publicValidateInput(method, { value: 123 })).toThrow(
116
+ /Invalid input for method "echo"/,
117
+ )
118
+ })
119
+
120
+ it('measureExecution returns the result + rounded durationMs', async () => {
121
+ const c = new TestConnector()
122
+ const { result, durationMs } = await c.publicMeasureExecution(async () => {
123
+ await new Promise((r) => setTimeout(r, 5))
124
+ return 42
125
+ })
126
+ expect(result).toBe(42)
127
+ expect(durationMs).toBeGreaterThanOrEqual(0)
128
+ expect(Number.isInteger(durationMs)).toBe(true)
129
+ })
130
+ })