@classytic/arc 2.41.1 → 2.43.0

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 (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +16 -12
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-YzIbF7Ii.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -1,12 +1,13 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { t as CRUD_OPERATIONS } from "../constants-D5Ubn1fe.mjs";
3
- import { g as getUserId } from "../types-CgenB1gv.mjs";
4
- import { t as BaseController } from "../BaseController-CYEwjyjN.mjs";
5
- import { t as defineAggregation } from "../defineAggregation-D_eJjm5A.mjs";
6
- import { t as defineResource } from "../defineResource-DxkMTjRn.mjs";
7
- import { E as requireOrgRole } from "../permissions-D8TBH44E.mjs";
8
- import { a as createApp, n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-f3boenVZ.mjs";
9
- import { n as getModuleExports } from "../module-DpLJG2_N.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as CRUD_OPERATIONS } from "../constants-Cu7Mx7_U.mjs";
3
+ import { c as BaseController } from "../plugin-BccET1tL.mjs";
4
+ import { g as getUserId } from "../types-Bk9NOgOC.mjs";
5
+ import { t as defineAggregation } from "../defineAggregation-Bm8RjMbq.mjs";
6
+ import { t as defineResource } from "../defineResource-DIC7JZGU.mjs";
7
+ import { E as requireOrgRole } from "../permissions-Cm0uYHvd.mjs";
8
+ import { h as asModulePorts, r as createApp } from "../createApp-D-953ZAq.mjs";
9
+ import { d as resolveModule, l as isLazyBridge, o as resolveModuleClosure } from "../plan-FVR6LP59.mjs";
10
+ import { n as getModuleExports } from "../module-BSvOQC7E.mjs";
10
11
  import { runStorageContract } from "./storageContract.mjs";
11
12
  import Fastify from "fastify";
12
13
  import { afterAll, describe, expect, it, vi } from "vitest";
@@ -134,6 +135,243 @@ function expectArc(response) {
134
135
  return assertion;
135
136
  }
136
137
  //#endregion
138
+ //#region src/testing/testActor.ts
139
+ /**
140
+ * Test-only actor installation — how a package's HTTP tests say "run this
141
+ * request as a manager in org X".
142
+ *
143
+ * Identity in arc comes from `request.scope` and only from `request.scope`;
144
+ * headers like `x-role` are not identity. That rule left tests with no seam —
145
+ * `bootModuleApp` boots with `auth: false`, so every injected request arrived as
146
+ * `PUBLIC_SCOPE` and any role-gated route answered 401 with no supported way to
147
+ * be somebody.
148
+ *
149
+ * The `x-test-actor` header is a TRANSPORT for the test's intent, not a trusted
150
+ * claim. The authenticator that reads it is installed by `bootModuleApp` alone,
151
+ * behind the `@classytic/arc/testing` entrypoint; `createApp` never registers
152
+ * it, so no production build turns a forged header into identity. It is
153
+ * per-request rather than boot-only because the interesting authorization tests
154
+ * are the ones where a cashier is refused what a manager is allowed.
155
+ *
156
+ * Gate the routes under test with arc's own primitives — `requireRoles`,
157
+ * `requireOrgMembership`, `allOf(...)`. A test-specific gate would be a second
158
+ * implementation of authorization whose semantics could drift from the one
159
+ * actually shipping.
160
+ */
161
+ /** The header the testing harness uses to carry a request's actor. */
162
+ const TEST_ACTOR_HEADER = "x-test-actor";
163
+ /**
164
+ * Build the headers that make an injected request act as `actor`.
165
+ *
166
+ * A string is shorthand for a single role. `null` means "stay public", so a test
167
+ * can prove an endpoint is reachable anonymously.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * t.app.inject({ method: 'GET', url: '/pos/shifts/current',
172
+ * headers: testActorHeaders('manager', ORG) });
173
+ * ```
174
+ */
175
+ function testActorHeaders(actor, organizationId) {
176
+ if (actor === null) return {};
177
+ const org = organizationId !== void 0 ? { organizationId } : {};
178
+ const spec = typeof actor === "string" ? {
179
+ roles: [actor],
180
+ ...org
181
+ } : {
182
+ ...actor,
183
+ ...org
184
+ };
185
+ return { [TEST_ACTOR_HEADER]: JSON.stringify(spec) };
186
+ }
187
+ /**
188
+ * Turn a spec into a `RequestScope`.
189
+ *
190
+ * `orgRoles` defaults to `roles`: a test that says "manager" means it at both
191
+ * levels, and making the caller repeat itself only produces tests that pass a
192
+ * global gate and mysteriously fail an org one.
193
+ */
194
+ function scopeFromTestActor(spec) {
195
+ const roles = spec.roles ?? [];
196
+ const userId = spec.userId ?? `test-${roles.join("-") || "anon"}`;
197
+ if (spec.elevated !== void 0 && spec.elevated !== false) return {
198
+ kind: "elevated",
199
+ userId,
200
+ elevatedBy: spec.elevated === true ? "test" : spec.elevated,
201
+ ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {},
202
+ ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
203
+ };
204
+ if (spec.organizationId !== void 0) return {
205
+ kind: "member",
206
+ userId,
207
+ userRoles: roles,
208
+ organizationId: spec.organizationId,
209
+ orgRoles: spec.orgRoles ?? roles,
210
+ ...spec.teamId !== void 0 ? { teamId: spec.teamId } : {},
211
+ ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
212
+ };
213
+ return {
214
+ kind: "authenticated",
215
+ userId,
216
+ userRoles: roles
217
+ };
218
+ }
219
+ /**
220
+ * Build the `auth` option that makes the test actor arc's authenticator.
221
+ *
222
+ * It must be an authenticator, not a hook. Fastify encapsulates plugins and
223
+ * modules register DURING `createApp`, so a hook added to the root instance
224
+ * afterwards never runs for their routes — every request still resolves as
225
+ * public and every role gate still answers 401, which reads exactly like a
226
+ * broken authorization rule in the code under test. Registering as
227
+ * `auth: { type: 'authenticator' }` puts the scope where arc's own auth would,
228
+ * at the phase arc expects, so tenant filtering and permission checks behave
229
+ * identically to production.
230
+ *
231
+ * `defaultActor` applies to requests carrying no header. A malformed header
232
+ * REJECTS rather than falling back to public — a test that meant to be an admin
233
+ * and got quietly demoted is the most expensive way to fail.
234
+ */
235
+ function testActorAuth(defaultActor) {
236
+ const authenticate = async (request) => {
237
+ const req = request;
238
+ const raw = req.headers[TEST_ACTOR_HEADER];
239
+ let spec = defaultActor;
240
+ if (typeof raw === "string") try {
241
+ spec = JSON.parse(raw);
242
+ } catch {
243
+ throw new Error(`[arc-testing] malformed ${TEST_ACTOR_HEADER} header: ${raw}. Build it with testActorHeaders() rather than by hand.`);
244
+ }
245
+ if (spec === void 0) return;
246
+ req.scope = scopeFromTestActor(spec);
247
+ req.user = {
248
+ id: getUserId(req.scope) ?? "test-anon",
249
+ ...spec.roles !== void 0 ? { role: spec.roles } : {},
250
+ ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {}
251
+ };
252
+ };
253
+ return {
254
+ type: "authenticator",
255
+ authenticate
256
+ };
257
+ }
258
+ //#endregion
259
+ //#region src/testing/bootModuleApp.ts
260
+ /**
261
+ * The built-in DB factory: in-memory MongoDB via lazy devDep imports.
262
+ * Everything Mongo-specific about the harness lives HERE — the harness
263
+ * itself is driver-blind and any kit can replace this via `database`.
264
+ */
265
+ async function mongoMemoryDatabase(opts) {
266
+ let mms;
267
+ try {
268
+ mms = await import("mongodb-memory-server");
269
+ } catch {
270
+ throw new Error("[arc-testing] bootModuleApp's default database is in-memory MongoDB — install 'mongodb-memory-server' + 'mongoose' as devDependencies, or inject your own `database` factory for a different store");
271
+ }
272
+ const server = opts.replset ? await mms.MongoMemoryReplSet.create({ replSet: { count: 1 } }) : await mms.MongoMemoryServer.create();
273
+ const uri = server.getUri();
274
+ const mongoose = (await import("mongoose")).default;
275
+ if (mongoose.connection.readyState !== 0) await mongoose.disconnect();
276
+ await mongoose.connect(uri);
277
+ mongoose.connection.deleteModel(/.*/);
278
+ return {
279
+ uri,
280
+ connection: mongoose.connection,
281
+ teardown: async () => {
282
+ if (mongoose.connection.readyState !== 0) await mongoose.disconnect().catch(() => void 0);
283
+ await server.stop().catch(() => void 0);
284
+ }
285
+ };
286
+ }
287
+ async function bootModuleApp(modules, options = {}) {
288
+ const db = await (options.database ?? mongoMemoryDatabase)({ replset: options.replset ?? false });
289
+ const ctx = {
290
+ uri: db.uri,
291
+ connection: db.connection
292
+ };
293
+ const resolved = typeof modules === "function" ? await modules(ctx) : modules;
294
+ const list = Array.isArray(resolved) ? resolved : [resolved];
295
+ let app;
296
+ try {
297
+ app = await createApp({
298
+ preset: "testing",
299
+ /**
300
+ * The test actor is arc's AUTHENTICATOR rather than a bolted-on hook, so the scope
301
+ * lands at the phase the rest of the pipeline expects — tenant filtering and
302
+ * permission checks then behave exactly as they do in production.
303
+ *
304
+ * This replaces the old `auth: false`. The authenticator is inert for a request
305
+ * carrying no `x-test-actor` header and no configured default, so a suite that
306
+ * never mentions an actor behaves as it did before. `options.app` still wins, so a
307
+ * suite exercising real auth can override it.
308
+ */
309
+ auth: testActorAuth(options.actor),
310
+ sensible: false,
311
+ ...options.app,
312
+ ...options.ports ? { ports: options.ports } : {},
313
+ modules: list
314
+ });
315
+ await app.ready();
316
+ } catch (err) {
317
+ await db.teardown().catch(() => void 0);
318
+ throw err;
319
+ }
320
+ let closed = false;
321
+ return {
322
+ app,
323
+ uri: db.uri,
324
+ connection: db.connection,
325
+ exports: (name) => getModuleExports(app, name),
326
+ close: async () => {
327
+ if (closed) return;
328
+ closed = true;
329
+ await app.close().catch(() => void 0);
330
+ await db.teardown().catch(() => void 0);
331
+ }
332
+ };
333
+ }
334
+ //#endregion
335
+ //#region src/testing/assertModuleBootsAlone.ts
336
+ /**
337
+ * Boot a module inside its closure — `resolveModuleClosure` (its `dependsOn` and
338
+ * `requires.modules`, transitively, plus every platform module) and every bridge in the list,
339
+ * which arc activates only when both ends are present — and fail naming the module when the boot
340
+ * or the `probe` throws. A hidden edge fails here, by name.
341
+ *
342
+ * ```ts
343
+ * it("order boots alone", () => assertModuleBootsAlone(allModules, "order", { probe: (t) => drivePorts(t) }));
344
+ * ```
345
+ */
346
+ async function assertModuleBootsAlone(modules, name, options = {}) {
347
+ const roots = typeof name === "string" ? [name] : name;
348
+ const label = roots.map((r) => `"${r}"`).join(" + ");
349
+ let closure = [];
350
+ try {
351
+ const { probe, ...bootOptions } = options;
352
+ const t = await bootModuleApp(async (ctx) => {
353
+ const resolved = typeof modules === "function" ? await modules(ctx) : modules;
354
+ const inputs = isInputList(resolved) ? resolved : [resolved];
355
+ const nodes = await Promise.all(inputs.map((input) => isLazyBridge(input) ? input : resolveModule(input)));
356
+ const included = new Set(resolveModuleClosure(nodes, roots).map((m) => m.name));
357
+ closure = nodes.filter((m) => included.has(m.name)).map((m) => m.name);
358
+ return nodes.filter((m) => included.has(m.name) || m.kind === "bridge");
359
+ }, bootOptions);
360
+ try {
361
+ await probe?.(t);
362
+ } finally {
363
+ await t.close();
364
+ }
365
+ } catch (err) {
366
+ const msg = err instanceof Error ? err.message : String(err);
367
+ throw new Error(`[arc/testing] module ${label} does not boot alone in its closure [${closure.join(", ") || "unresolved"}]: ${msg}`, { cause: err });
368
+ }
369
+ return { closure };
370
+ }
371
+ function isInputList(value) {
372
+ return Array.isArray(value);
373
+ }
374
+ //#endregion
137
375
  //#region src/testing/mocks.ts
138
376
  /**
139
377
  * Create a mock repository for testing
@@ -833,202 +1071,6 @@ async function setupBetterAuthTestApp(input) {
833
1071
  };
834
1072
  }
835
1073
  //#endregion
836
- //#region src/testing/testActor.ts
837
- /**
838
- * Test-only actor installation — how a package's HTTP tests say "run this
839
- * request as a manager in org X".
840
- *
841
- * Identity in arc comes from `request.scope` and only from `request.scope`;
842
- * headers like `x-role` are not identity. That rule left tests with no seam —
843
- * `bootModuleApp` boots with `auth: false`, so every injected request arrived as
844
- * `PUBLIC_SCOPE` and any role-gated route answered 401 with no supported way to
845
- * be somebody.
846
- *
847
- * The `x-test-actor` header is a TRANSPORT for the test's intent, not a trusted
848
- * claim. The authenticator that reads it is installed by `bootModuleApp` alone,
849
- * behind the `@classytic/arc/testing` entrypoint; `createApp` never registers
850
- * it, so no production build turns a forged header into identity. It is
851
- * per-request rather than boot-only because the interesting authorization tests
852
- * are the ones where a cashier is refused what a manager is allowed.
853
- *
854
- * Gate the routes under test with arc's own primitives — `requireRoles`,
855
- * `requireOrgMembership`, `allOf(...)`. A test-specific gate would be a second
856
- * implementation of authorization whose semantics could drift from the one
857
- * actually shipping.
858
- */
859
- /** The header the testing harness uses to carry a request's actor. */
860
- const TEST_ACTOR_HEADER = "x-test-actor";
861
- /**
862
- * Build the headers that make an injected request act as `actor`.
863
- *
864
- * A string is shorthand for a single role. `null` means "stay public", so a test
865
- * can prove an endpoint is reachable anonymously.
866
- *
867
- * @example
868
- * ```ts
869
- * t.app.inject({ method: 'GET', url: '/pos/shifts/current',
870
- * headers: testActorHeaders('manager', ORG) });
871
- * ```
872
- */
873
- function testActorHeaders(actor, organizationId) {
874
- if (actor === null) return {};
875
- const org = organizationId !== void 0 ? { organizationId } : {};
876
- const spec = typeof actor === "string" ? {
877
- roles: [actor],
878
- ...org
879
- } : {
880
- ...actor,
881
- ...org
882
- };
883
- return { [TEST_ACTOR_HEADER]: JSON.stringify(spec) };
884
- }
885
- /**
886
- * Turn a spec into a `RequestScope`.
887
- *
888
- * `orgRoles` defaults to `roles`: a test that says "manager" means it at both
889
- * levels, and making the caller repeat itself only produces tests that pass a
890
- * global gate and mysteriously fail an org one.
891
- */
892
- function scopeFromTestActor(spec) {
893
- const roles = spec.roles ?? [];
894
- const userId = spec.userId ?? `test-${roles.join("-") || "anon"}`;
895
- if (spec.elevated !== void 0 && spec.elevated !== false) return {
896
- kind: "elevated",
897
- userId,
898
- elevatedBy: spec.elevated === true ? "test" : spec.elevated,
899
- ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {},
900
- ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
901
- };
902
- if (spec.organizationId !== void 0) return {
903
- kind: "member",
904
- userId,
905
- userRoles: roles,
906
- organizationId: spec.organizationId,
907
- orgRoles: spec.orgRoles ?? roles,
908
- ...spec.teamId !== void 0 ? { teamId: spec.teamId } : {},
909
- ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
910
- };
911
- return {
912
- kind: "authenticated",
913
- userId,
914
- userRoles: roles
915
- };
916
- }
917
- /**
918
- * Build the `auth` option that makes the test actor arc's authenticator.
919
- *
920
- * It must be an authenticator, not a hook. Fastify encapsulates plugins and
921
- * modules register DURING `createApp`, so a hook added to the root instance
922
- * afterwards never runs for their routes — every request still resolves as
923
- * public and every role gate still answers 401, which reads exactly like a
924
- * broken authorization rule in the code under test. Registering as
925
- * `auth: { type: 'authenticator' }` puts the scope where arc's own auth would,
926
- * at the phase arc expects, so tenant filtering and permission checks behave
927
- * identically to production.
928
- *
929
- * `defaultActor` applies to requests carrying no header. A malformed header
930
- * REJECTS rather than falling back to public — a test that meant to be an admin
931
- * and got quietly demoted is the most expensive way to fail.
932
- */
933
- function testActorAuth(defaultActor) {
934
- const authenticate = async (request) => {
935
- const req = request;
936
- const raw = req.headers[TEST_ACTOR_HEADER];
937
- let spec = defaultActor;
938
- if (typeof raw === "string") try {
939
- spec = JSON.parse(raw);
940
- } catch {
941
- throw new Error(`[arc-testing] malformed ${TEST_ACTOR_HEADER} header: ${raw}. Build it with testActorHeaders() rather than by hand.`);
942
- }
943
- if (spec === void 0) return;
944
- req.scope = scopeFromTestActor(spec);
945
- req.user = {
946
- id: getUserId(req.scope) ?? "test-anon",
947
- ...spec.roles !== void 0 ? { role: spec.roles } : {},
948
- ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {}
949
- };
950
- };
951
- return {
952
- type: "authenticator",
953
- authenticate
954
- };
955
- }
956
- //#endregion
957
- //#region src/testing/bootModuleApp.ts
958
- /**
959
- * The built-in DB factory: in-memory MongoDB via lazy devDep imports.
960
- * Everything Mongo-specific about the harness lives HERE — the harness
961
- * itself is driver-blind and any kit can replace this via `database`.
962
- */
963
- async function mongoMemoryDatabase(opts) {
964
- let mms;
965
- try {
966
- mms = await import("mongodb-memory-server");
967
- } catch {
968
- throw new Error("[arc-testing] bootModuleApp's default database is in-memory MongoDB — install 'mongodb-memory-server' + 'mongoose' as devDependencies, or inject your own `database` factory for a different store");
969
- }
970
- const server = opts.replset ? await mms.MongoMemoryReplSet.create({ replSet: { count: 1 } }) : await mms.MongoMemoryServer.create();
971
- const uri = server.getUri();
972
- const mongoose = (await import("mongoose")).default;
973
- if (mongoose.connection.readyState !== 0) await mongoose.disconnect();
974
- await mongoose.connect(uri);
975
- mongoose.connection.deleteModel(/.*/);
976
- return {
977
- uri,
978
- connection: mongoose.connection,
979
- teardown: async () => {
980
- if (mongoose.connection.readyState !== 0) await mongoose.disconnect().catch(() => void 0);
981
- await server.stop().catch(() => void 0);
982
- }
983
- };
984
- }
985
- async function bootModuleApp(modules, options = {}) {
986
- const db = await (options.database ?? mongoMemoryDatabase)({ replset: options.replset ?? false });
987
- const ctx = {
988
- uri: db.uri,
989
- connection: db.connection
990
- };
991
- const resolved = typeof modules === "function" ? await modules(ctx) : modules;
992
- const list = Array.isArray(resolved) ? resolved : [resolved];
993
- let app;
994
- try {
995
- app = await createApp({
996
- preset: "testing",
997
- /**
998
- * The test actor is arc's AUTHENTICATOR rather than a bolted-on hook, so the scope
999
- * lands at the phase the rest of the pipeline expects — tenant filtering and
1000
- * permission checks then behave exactly as they do in production.
1001
- *
1002
- * This replaces the old `auth: false`. The authenticator is inert for a request
1003
- * carrying no `x-test-actor` header and no configured default, so a suite that
1004
- * never mentions an actor behaves as it did before. `options.app` still wins, so a
1005
- * suite exercising real auth can override it.
1006
- */
1007
- auth: testActorAuth(options.actor),
1008
- sensible: false,
1009
- ...options.app,
1010
- modules: list
1011
- });
1012
- await app.ready();
1013
- } catch (err) {
1014
- await db.teardown().catch(() => void 0);
1015
- throw err;
1016
- }
1017
- let closed = false;
1018
- return {
1019
- app,
1020
- uri: db.uri,
1021
- connection: db.connection,
1022
- exports: (name) => getModuleExports(app, name),
1023
- close: async () => {
1024
- if (closed) return;
1025
- closed = true;
1026
- await app.close().catch(() => void 0);
1027
- await db.teardown().catch(() => void 0);
1028
- }
1029
- };
1030
- }
1031
- //#endregion
1032
1074
  //#region src/testing/fixtures.ts
1033
1075
  function createTestFixtures() {
1034
1076
  const registry = /* @__PURE__ */ new Map();
@@ -1356,13 +1398,45 @@ function createHttpTestHarness(resource, optionsOrGetter) {
1356
1398
  }
1357
1399
  //#endregion
1358
1400
  //#region src/testing/moduleSetup.ts
1359
- function createTestModuleSetup() {
1401
+ /**
1402
+ * A real `ModuleSetupContext` for tests that invoke a module's setup phases
1403
+ * DIRECTLY — `mod.bootstrap(app, ctx)`, `mod.plugins(app, ctx)` — instead of
1404
+ * booting a whole app through `createApp` / `bootModuleApp`.
1405
+ *
1406
+ * ## Why this is not `{ defer: () => {} }`
1407
+ *
1408
+ * That literal satisfies the type and throws away every disposer the module
1409
+ * registers. A test written against it passes whether the module releases what
1410
+ * it acquired or leaks it, so the one thing `defer` exists to make testable
1411
+ * becomes the one thing that cannot be tested. It also gives a false negative
1412
+ * in the other direction: a module that starts deferring later keeps its green
1413
+ * test, which is precisely the "compiles today, wrong tomorrow" shape the
1414
+ * disposer contract was introduced to remove.
1415
+ *
1416
+ * This collects them instead, and `dispose()` unwinds LIFO — the same order
1417
+ * arc's own teardown uses, so an ordering bug surfaces here rather than in
1418
+ * production shutdown.
1419
+ *
1420
+ * @example
1421
+ * ```ts
1422
+ * const setup = createTestModuleSetup();
1423
+ * const engine = await mod.bootstrap?.(app, setup.context);
1424
+ *
1425
+ * expect(setup.deferred).toHaveLength(1); // it registered its teardown
1426
+ * await setup.dispose(); // and it actually releases
1427
+ * expect(engine.closed).toBe(true);
1428
+ * ```
1429
+ */
1430
+ function createTestModuleSetup(options = {}) {
1360
1431
  const deferred = [];
1361
1432
  return {
1362
- context: { defer(disposer) {
1363
- if (typeof disposer !== "function") throw new TypeError(`[arc/testing] defer() expected a function, received ${typeof disposer}.`);
1364
- deferred.push(disposer);
1365
- } },
1433
+ context: {
1434
+ ports: asModulePorts(Object.freeze({ ...options.ports })),
1435
+ defer(disposer) {
1436
+ if (typeof disposer !== "function") throw new TypeError(`[arc/testing] defer() expected a function, received ${typeof disposer}.`);
1437
+ deferred.push(disposer);
1438
+ }
1439
+ },
1366
1440
  deferred,
1367
1441
  async dispose() {
1368
1442
  const errors = [];
@@ -1504,7 +1578,7 @@ function pickDefaultAuth(authMode, callerAuth) {
1504
1578
  };
1505
1579
  }
1506
1580
  async function createTestApp(options = {}) {
1507
- const { createApp } = await import("../preloadResources-f3boenVZ.mjs").then((n) => n.o);
1581
+ const { createApp } = await import("../createApp-D-953ZAq.mjs").then((n) => n.i);
1508
1582
  const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
1509
1583
  let dbHandle;
1510
1584
  let dbUri;
@@ -1574,4 +1648,4 @@ function createMinimalTestApp(options = {}) {
1574
1648
  });
1575
1649
  }
1576
1650
  //#endregion
1577
- export { HttpTestHarness, TEST_ACTOR_HEADER, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestModuleSetup, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, scopeFromTestActor, scopeRoleGate, setupBetterAuthTestApp, testActorAuth, testActorHeaders, waitFor };
1651
+ export { HttpTestHarness, TEST_ACTOR_HEADER, assertModuleBootsAlone, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestModuleSetup, createTestTimer, expectArc, mongoMemoryDatabase, runAuthorizationConformance, runStorageContract, safeParseBody, scopeFromTestActor, scopeRoleGate, setupBetterAuthTestApp, testActorAuth, testActorHeaders, waitFor };
@@ -1,5 +1,6 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { OutboxOwnershipError } from "@classytic/primitives/outbox";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as claimedAttempts } from "../claimed-B10RTfkM.mjs";
3
+ import { OutboxConflictError, OutboxOwnershipError } from "@classytic/primitives/outbox";
3
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
5
  //#region src/testing/outboxStoreContract.ts
5
6
  let seq = 0;
@@ -81,6 +82,26 @@ function runOutboxStoreContract(name, setup) {
81
82
  expect((await store.getPending(10)).length).toBe(0);
82
83
  });
83
84
  });
85
+ describe("save — one row per event id", () => {
86
+ it("a second save of the same id and payload is a no-op", async () => {
87
+ const e = event();
88
+ await store.save(e);
89
+ await store.save({
90
+ ...e,
91
+ meta: { ...e.meta }
92
+ });
93
+ expect((await store.getPending(10)).map(idOf)).toEqual([idOf(e)]);
94
+ });
95
+ it("THROWS OutboxConflictError when the same id arrives with a different payload", async () => {
96
+ const e = event();
97
+ await store.save(e);
98
+ await expect(store.save({
99
+ ...e,
100
+ payload: { n: -1 }
101
+ })).rejects.toBeInstanceOf(OutboxConflictError);
102
+ expect((await store.getPending(10)).length).toBe(1);
103
+ });
104
+ });
84
105
  describe("getPending", () => {
85
106
  it("returns events FIFO (oldest first)", async () => {
86
107
  const saved = await seed(3);
@@ -164,6 +185,120 @@ function runOutboxStoreContract(name, setup) {
164
185
  expect(claimed.map(idOf)).toEqual(saved.slice(0, 2).map(idOf));
165
186
  });
166
187
  });
188
+ /**
189
+ * The fenced claim had NO coverage here, though it is the path the relay
190
+ * prefers and the one fencing exists for. Two guarantees, both silent when
191
+ * broken:
192
+ * - a re-claim after lease expiry mints a STRICTLY GREATER token, so the
193
+ * crashed-and-resumed ex-holder cannot acknowledge an event it no longer
194
+ * owns;
195
+ * - `attempts`, when a store reports it, is the DURABLE count — it must
196
+ * rise across claims by different consumers, because a count that each
197
+ * relay keeps for itself is the thing it exists to replace.
198
+ */
199
+ describe("claimPendingFenced — fencing tokens + durable attempts", () => {
200
+ it("mints a STRICTLY GREATER token when a lease is taken over", async ({ skip }) => {
201
+ if (!store.claimPendingFenced) return skip();
202
+ await seed(1);
203
+ const [first] = await store.claimPendingFenced({
204
+ consumerId: "dead",
205
+ limit: 1,
206
+ leaseMs: 0
207
+ });
208
+ const [second] = await store.claimPendingFenced({
209
+ consumerId: "c2",
210
+ limit: 1
211
+ });
212
+ if (!first || !second) return skip();
213
+ expect(second.fencingToken).toBeGreaterThan(first.fencingToken);
214
+ });
215
+ it("REJECTS an acknowledge carrying the superseded token", async ({ skip }) => {
216
+ if (!store.claimPendingFenced) return skip();
217
+ await seed(1);
218
+ const [stale] = await store.claimPendingFenced({
219
+ consumerId: "dead",
220
+ limit: 1,
221
+ leaseMs: 0
222
+ });
223
+ const [current] = await store.claimPendingFenced({
224
+ consumerId: "c2",
225
+ limit: 1
226
+ });
227
+ if (!stale || !current) return skip();
228
+ await expect(store.acknowledge(idOf(stale.event), { fencingToken: stale.fencingToken })).rejects.toBeInstanceOf(OutboxOwnershipError);
229
+ });
230
+ it("reports attempts = 1 on the first claim, when it reports attempts at all", async ({ skip }) => {
231
+ if (!store.claimPendingFenced) return skip();
232
+ await seed(1);
233
+ const [claimed] = await store.claimPendingFenced({
234
+ consumerId: "c1",
235
+ limit: 1
236
+ });
237
+ const attempts = claimed ? claimedAttempts(claimed) : void 0;
238
+ if (attempts === void 0) return skip();
239
+ expect(attempts).toBe(1);
240
+ });
241
+ it("attempts is DURABLE — it rises across claims by DIFFERENT consumers", async ({ skip }) => {
242
+ if (!store.claimPendingFenced || !store.fail) return skip();
243
+ await seed(1);
244
+ const seen = [];
245
+ for (const consumerId of [
246
+ "relay-a",
247
+ "relay-b",
248
+ "relay-c"
249
+ ]) {
250
+ const [claimed] = await store.claimPendingFenced({
251
+ consumerId,
252
+ limit: 1
253
+ });
254
+ if (!claimed) return skip();
255
+ const attempts = claimedAttempts(claimed);
256
+ if (attempts === void 0) return skip();
257
+ seen.push(attempts);
258
+ await store.fail(idOf(claimed.event), { message: "boom" }, {
259
+ consumerId,
260
+ fencingToken: claimed.fencingToken
261
+ });
262
+ }
263
+ expect(seen).toEqual([
264
+ 1,
265
+ 2,
266
+ 3
267
+ ]);
268
+ });
269
+ it("requeue RESETS attempts — a re-driven event starts over", async ({ skip }) => {
270
+ if (!store.claimPendingFenced || !store.fail || !store.requeue) return skip();
271
+ await seed(1);
272
+ const [claimed] = await store.claimPendingFenced({
273
+ consumerId: "c1",
274
+ limit: 1
275
+ });
276
+ if (!claimed || claimedAttempts(claimed) === void 0) return skip();
277
+ await store.fail(idOf(claimed.event), { message: "fatal" }, {
278
+ consumerId: "c1",
279
+ fencingToken: claimed.fencingToken,
280
+ deadLetter: true
281
+ });
282
+ expect(await store.requeue(idOf(claimed.event))).toBe(true);
283
+ const [again] = await store.claimPendingFenced({
284
+ consumerId: "c2",
285
+ limit: 1
286
+ });
287
+ expect(again ? claimedAttempts(again) : void 0).toBe(1);
288
+ });
289
+ });
290
+ describe("acknowledgeMany — one write per flush", () => {
291
+ it("marks every listed event delivered and ignores unknown ids", async ({ skip }) => {
292
+ if (!store.acknowledgeMany) return skip();
293
+ const events = await seed(3);
294
+ await store.acknowledgeMany([
295
+ idOf(events[0]),
296
+ idOf(events[2]),
297
+ "nope"
298
+ ]);
299
+ expect((await store.getPending(10)).map(idOf)).toEqual([idOf(events[1])]);
300
+ });
301
+ });
167
302
  describe("acknowledge", () => {
168
303
  it("removes the event from pending", async () => {
169
304
  const [e] = await seed(1);