@beignet/core 0.0.3 → 0.0.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 (360) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +785 -43
  3. package/dist/application/index.d.ts +28 -2
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +140 -12
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/client/client.d.ts +2 -2
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +136 -48
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/error-messages.d.ts +14 -0
  12. package/dist/client/error-messages.d.ts.map +1 -0
  13. package/dist/client/error-messages.js +23 -0
  14. package/dist/client/error-messages.js.map +1 -0
  15. package/dist/client/index.d.ts +8 -4
  16. package/dist/client/index.d.ts.map +1 -1
  17. package/dist/client/index.js +6 -2
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/types.d.ts +35 -5
  20. package/dist/client/types.d.ts.map +1 -1
  21. package/dist/client-only.d.ts +8 -0
  22. package/dist/client-only.d.ts.map +1 -0
  23. package/dist/client-only.js +8 -0
  24. package/dist/client-only.js.map +1 -0
  25. package/dist/config/index.d.ts +5 -5
  26. package/dist/config/index.d.ts.map +1 -1
  27. package/dist/config/index.js +2 -2
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/contracts/catalog-errors.d.ts +27 -0
  30. package/dist/contracts/catalog-errors.d.ts.map +1 -0
  31. package/dist/contracts/catalog-errors.js +69 -0
  32. package/dist/contracts/catalog-errors.js.map +1 -0
  33. package/dist/contracts/contract-builder.d.ts +15 -12
  34. package/dist/contracts/contract-builder.d.ts.map +1 -1
  35. package/dist/contracts/contract-builder.js +15 -41
  36. package/dist/contracts/contract-builder.js.map +1 -1
  37. package/dist/contracts/contract-group.d.ts +11 -8
  38. package/dist/contracts/contract-group.d.ts.map +1 -1
  39. package/dist/contracts/contract-group.js +13 -40
  40. package/dist/contracts/contract-group.js.map +1 -1
  41. package/dist/contracts/contract-like.d.ts +1 -1
  42. package/dist/contracts/contract-like.d.ts.map +1 -1
  43. package/dist/contracts/index.d.ts +13 -9
  44. package/dist/contracts/index.d.ts.map +1 -1
  45. package/dist/contracts/index.js +9 -5
  46. package/dist/contracts/index.js.map +1 -1
  47. package/dist/contracts/openapi-meta.d.ts +48 -0
  48. package/dist/contracts/openapi-meta.d.ts.map +1 -1
  49. package/dist/contracts/openapi-meta.js +3 -0
  50. package/dist/contracts/openapi-meta.js.map +1 -1
  51. package/dist/contracts/path-template.d.ts +1 -1
  52. package/dist/contracts/path-template.js +2 -2
  53. package/dist/contracts/path-template.js.map +1 -1
  54. package/dist/contracts/schema-shape.d.ts +37 -0
  55. package/dist/contracts/schema-shape.d.ts.map +1 -0
  56. package/dist/contracts/schema-shape.js +61 -0
  57. package/dist/contracts/schema-shape.js.map +1 -0
  58. package/dist/contracts/success-status.d.ts +32 -0
  59. package/dist/contracts/success-status.d.ts.map +1 -0
  60. package/dist/contracts/success-status.js +18 -0
  61. package/dist/contracts/success-status.js.map +1 -0
  62. package/dist/contracts/types.d.ts +25 -5
  63. package/dist/contracts/types.d.ts.map +1 -1
  64. package/dist/contracts/types.js.map +1 -1
  65. package/dist/contracts/utils.d.ts +1 -1
  66. package/dist/contracts/utils.d.ts.map +1 -1
  67. package/dist/contracts/utils.js +1 -1
  68. package/dist/contracts/utils.js.map +1 -1
  69. package/dist/domain/events.d.ts +1 -1
  70. package/dist/domain/events.d.ts.map +1 -1
  71. package/dist/domain/events.js +1 -1
  72. package/dist/domain/events.js.map +1 -1
  73. package/dist/domain/index.d.ts +3 -3
  74. package/dist/domain/index.d.ts.map +1 -1
  75. package/dist/domain/index.js +3 -3
  76. package/dist/domain/index.js.map +1 -1
  77. package/dist/errors/catalog.d.ts +9 -1
  78. package/dist/errors/catalog.d.ts.map +1 -1
  79. package/dist/errors/catalog.js +7 -1
  80. package/dist/errors/catalog.js.map +1 -1
  81. package/dist/errors/http.d.ts +10 -0
  82. package/dist/errors/http.d.ts.map +1 -1
  83. package/dist/errors/http.js +11 -1
  84. package/dist/errors/http.js.map +1 -1
  85. package/dist/errors/index.d.ts +4 -4
  86. package/dist/errors/index.d.ts.map +1 -1
  87. package/dist/errors/index.js +4 -4
  88. package/dist/errors/index.js.map +1 -1
  89. package/dist/errors/response.d.ts +4 -1
  90. package/dist/errors/response.d.ts.map +1 -1
  91. package/dist/errors/response.js.map +1 -1
  92. package/dist/events/index.d.ts +10 -12
  93. package/dist/events/index.d.ts.map +1 -1
  94. package/dist/events/index.js +10 -10
  95. package/dist/events/index.js.map +1 -1
  96. package/dist/idempotency/index.d.ts +5 -3
  97. package/dist/idempotency/index.d.ts.map +1 -1
  98. package/dist/idempotency/index.js.map +1 -1
  99. package/dist/jobs/index.d.ts +12 -14
  100. package/dist/jobs/index.d.ts.map +1 -1
  101. package/dist/jobs/index.js +13 -13
  102. package/dist/jobs/index.js.map +1 -1
  103. package/dist/notifications/index.d.ts +14 -16
  104. package/dist/notifications/index.d.ts.map +1 -1
  105. package/dist/notifications/index.js +14 -14
  106. package/dist/notifications/index.js.map +1 -1
  107. package/dist/openapi/index.d.ts +8 -3
  108. package/dist/openapi/index.d.ts.map +1 -1
  109. package/dist/openapi/index.js +41 -29
  110. package/dist/openapi/index.js.map +1 -1
  111. package/dist/openapi/schema-introspector.d.ts +37 -0
  112. package/dist/openapi/schema-introspector.d.ts.map +1 -1
  113. package/dist/openapi/schema-introspector.js +23 -17
  114. package/dist/openapi/schema-introspector.js.map +1 -1
  115. package/dist/outbox/index.d.ts +15 -6
  116. package/dist/outbox/index.d.ts.map +1 -1
  117. package/dist/outbox/index.js +60 -16
  118. package/dist/outbox/index.js.map +1 -1
  119. package/dist/ports/audit.d.ts +56 -10
  120. package/dist/ports/audit.d.ts.map +1 -1
  121. package/dist/ports/audit.js +71 -3
  122. package/dist/ports/audit.js.map +1 -1
  123. package/dist/ports/auth.d.ts +92 -0
  124. package/dist/ports/auth.d.ts.map +1 -1
  125. package/dist/ports/auth.js +92 -0
  126. package/dist/ports/auth.js.map +1 -1
  127. package/dist/ports/events.d.ts +2 -2
  128. package/dist/ports/events.d.ts.map +1 -1
  129. package/dist/ports/index.d.ts +62 -33
  130. package/dist/ports/index.d.ts.map +1 -1
  131. package/dist/ports/index.js +28 -34
  132. package/dist/ports/index.js.map +1 -1
  133. package/dist/ports/policy.d.ts +32 -3
  134. package/dist/ports/policy.d.ts.map +1 -1
  135. package/dist/ports/policy.js +13 -2
  136. package/dist/ports/policy.js.map +1 -1
  137. package/dist/ports/testing.d.ts +1030 -2
  138. package/dist/ports/testing.d.ts.map +1 -1
  139. package/dist/ports/testing.js +1031 -1
  140. package/dist/ports/testing.js.map +1 -1
  141. package/dist/ports/unbound.d.ts +21 -0
  142. package/dist/ports/unbound.d.ts.map +1 -0
  143. package/dist/ports/unbound.js +57 -0
  144. package/dist/ports/unbound.js.map +1 -0
  145. package/dist/ports/unit-of-work.d.ts +1 -1
  146. package/dist/ports/unit-of-work.d.ts.map +1 -1
  147. package/dist/ports/unit-of-work.js +1 -1
  148. package/dist/ports/unit-of-work.js.map +1 -1
  149. package/dist/providers/index.d.ts +3 -2
  150. package/dist/providers/index.d.ts.map +1 -1
  151. package/dist/providers/index.js +3 -2
  152. package/dist/providers/index.js.map +1 -1
  153. package/dist/providers/instrumentation.d.ts +45 -4
  154. package/dist/providers/instrumentation.d.ts.map +1 -1
  155. package/dist/providers/instrumentation.js +25 -6
  156. package/dist/providers/instrumentation.js.map +1 -1
  157. package/dist/providers/metadata.d.ts +39 -0
  158. package/dist/providers/metadata.d.ts.map +1 -0
  159. package/dist/providers/metadata.js +169 -0
  160. package/dist/providers/metadata.js.map +1 -0
  161. package/dist/providers/provider.d.ts +114 -9
  162. package/dist/providers/provider.d.ts.map +1 -1
  163. package/dist/providers/provider.js +3 -20
  164. package/dist/providers/provider.js.map +1 -1
  165. package/dist/schedules/index.d.ts +94 -13
  166. package/dist/schedules/index.d.ts.map +1 -1
  167. package/dist/schedules/index.js +66 -12
  168. package/dist/schedules/index.js.map +1 -1
  169. package/dist/server/audit-context.d.ts +29 -0
  170. package/dist/server/audit-context.d.ts.map +1 -0
  171. package/dist/server/audit-context.js +44 -0
  172. package/dist/server/audit-context.js.map +1 -0
  173. package/dist/server/context.d.ts +141 -0
  174. package/dist/server/context.d.ts.map +1 -0
  175. package/dist/server/context.js +39 -0
  176. package/dist/server/context.js.map +1 -0
  177. package/dist/server/contract-like.d.ts +1 -1
  178. package/dist/server/contract-like.d.ts.map +1 -1
  179. package/dist/server/contract-like.js +1 -1
  180. package/dist/server/contract-like.js.map +1 -1
  181. package/dist/server/health.d.ts +2 -2
  182. package/dist/server/health.d.ts.map +1 -1
  183. package/dist/server/hooks/auth.d.ts +49 -10
  184. package/dist/server/hooks/auth.d.ts.map +1 -1
  185. package/dist/server/hooks/auth.js +77 -37
  186. package/dist/server/hooks/auth.js.map +1 -1
  187. package/dist/server/hooks/cors.d.ts +1 -1
  188. package/dist/server/hooks/cors.d.ts.map +1 -1
  189. package/dist/server/hooks/errors.d.ts +2 -2
  190. package/dist/server/hooks/errors.d.ts.map +1 -1
  191. package/dist/server/hooks/errors.js +2 -2
  192. package/dist/server/hooks/errors.js.map +1 -1
  193. package/dist/server/hooks/idempotency.d.ts +78 -0
  194. package/dist/server/hooks/idempotency.d.ts.map +1 -0
  195. package/dist/server/hooks/idempotency.js +154 -0
  196. package/dist/server/hooks/idempotency.js.map +1 -0
  197. package/dist/server/hooks/index.d.ts +8 -7
  198. package/dist/server/hooks/index.d.ts.map +1 -1
  199. package/dist/server/hooks/index.js +6 -5
  200. package/dist/server/hooks/index.js.map +1 -1
  201. package/dist/server/hooks/logging.d.ts +2 -2
  202. package/dist/server/hooks/logging.d.ts.map +1 -1
  203. package/dist/server/hooks/logging.js +1 -1
  204. package/dist/server/hooks/logging.js.map +1 -1
  205. package/dist/server/hooks/rate-limit.d.ts +25 -7
  206. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  207. package/dist/server/hooks/rate-limit.js +47 -12
  208. package/dist/server/hooks/rate-limit.js.map +1 -1
  209. package/dist/server/hooks.d.ts +1 -1
  210. package/dist/server/hooks.d.ts.map +1 -1
  211. package/dist/server/hooks.js +1 -1
  212. package/dist/server/hooks.js.map +1 -1
  213. package/dist/server/http.d.ts +61 -35
  214. package/dist/server/http.d.ts.map +1 -1
  215. package/dist/server/http.js +1 -20
  216. package/dist/server/http.js.map +1 -1
  217. package/dist/server/index.d.ts +36 -12
  218. package/dist/server/index.d.ts.map +1 -1
  219. package/dist/server/index.js +24 -8
  220. package/dist/server/index.js.map +1 -1
  221. package/dist/server/instrumentation.d.ts +108 -0
  222. package/dist/server/instrumentation.d.ts.map +1 -0
  223. package/dist/server/instrumentation.js +297 -0
  224. package/dist/server/instrumentation.js.map +1 -0
  225. package/dist/server/openapi.d.ts +3 -3
  226. package/dist/server/openapi.d.ts.map +1 -1
  227. package/dist/server/openapi.js +1 -1
  228. package/dist/server/openapi.js.map +1 -1
  229. package/dist/server/providers/index.d.ts +3 -3
  230. package/dist/server/providers/index.d.ts.map +1 -1
  231. package/dist/server/providers/index.js +3 -3
  232. package/dist/server/providers/index.js.map +1 -1
  233. package/dist/server/providers/loadProviderConfig.d.ts +2 -2
  234. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  235. package/dist/server/providers/loadProviderConfig.js +2 -2
  236. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  237. package/dist/server/request-context.d.ts +67 -0
  238. package/dist/server/request-context.d.ts.map +1 -0
  239. package/dist/server/request-context.js +79 -0
  240. package/dist/server/request-context.js.map +1 -0
  241. package/dist/server/server-context.d.ts +38 -0
  242. package/dist/server/server-context.d.ts.map +1 -0
  243. package/dist/server/server-context.js +38 -0
  244. package/dist/server/server-context.js.map +1 -0
  245. package/dist/server/server.d.ts +105 -33
  246. package/dist/server/server.d.ts.map +1 -1
  247. package/dist/server/server.js +434 -118
  248. package/dist/server/server.js.map +1 -1
  249. package/dist/server/types.d.ts +2 -2
  250. package/dist/server/types.d.ts.map +1 -1
  251. package/dist/server/types.js +2 -2
  252. package/dist/server/types.js.map +1 -1
  253. package/dist/server/use-case-route.d.ts +263 -0
  254. package/dist/server/use-case-route.d.ts.map +1 -0
  255. package/dist/server/use-case-route.js +77 -0
  256. package/dist/server/use-case-route.js.map +1 -0
  257. package/dist/server-only.d.ts +8 -0
  258. package/dist/server-only.d.ts.map +1 -0
  259. package/dist/server-only.js +8 -0
  260. package/dist/server-only.js.map +1 -0
  261. package/dist/tasks/index.d.ts +139 -0
  262. package/dist/tasks/index.d.ts.map +1 -0
  263. package/dist/tasks/index.js +98 -0
  264. package/dist/tasks/index.js.map +1 -0
  265. package/dist/testing/index.d.ts +607 -5
  266. package/dist/testing/index.d.ts.map +1 -1
  267. package/dist/testing/index.js +426 -4
  268. package/dist/testing/index.js.map +1 -1
  269. package/dist/tracing/index.d.ts +89 -0
  270. package/dist/tracing/index.d.ts.map +1 -0
  271. package/dist/tracing/index.js +101 -0
  272. package/dist/tracing/index.js.map +1 -0
  273. package/dist/uploads/client.d.ts +1 -1
  274. package/dist/uploads/client.d.ts.map +1 -1
  275. package/dist/uploads/index.d.ts +2 -2
  276. package/dist/uploads/index.d.ts.map +1 -1
  277. package/dist/uploads/index.js +1 -1
  278. package/dist/uploads/index.js.map +1 -1
  279. package/package.json +24 -2
  280. package/src/application/index.ts +193 -10
  281. package/src/client/client.ts +148 -150
  282. package/src/client/error-messages.ts +35 -0
  283. package/src/client/index.ts +12 -4
  284. package/src/client/types.ts +44 -5
  285. package/src/client-only.ts +7 -0
  286. package/src/config/index.ts +6 -6
  287. package/src/contracts/catalog-errors.ts +115 -0
  288. package/src/contracts/contract-builder.ts +39 -76
  289. package/src/contracts/contract-group.ts +33 -68
  290. package/src/contracts/contract-like.ts +1 -1
  291. package/src/contracts/index.ts +24 -11
  292. package/src/contracts/openapi-meta.ts +55 -0
  293. package/src/contracts/path-template.ts +2 -2
  294. package/src/contracts/schema-shape.ts +75 -0
  295. package/src/contracts/success-status.ts +68 -0
  296. package/src/contracts/types.ts +32 -5
  297. package/src/contracts/utils.ts +5 -2
  298. package/src/domain/events.ts +6 -2
  299. package/src/domain/index.ts +3 -3
  300. package/src/errors/catalog.ts +9 -1
  301. package/src/errors/http.ts +11 -1
  302. package/src/errors/index.ts +4 -4
  303. package/src/errors/response.ts +4 -1
  304. package/src/events/index.ts +12 -26
  305. package/src/idempotency/index.ts +5 -3
  306. package/src/jobs/index.ts +14 -24
  307. package/src/notifications/index.ts +17 -27
  308. package/src/openapi/index.ts +73 -38
  309. package/src/openapi/schema-introspector.ts +68 -17
  310. package/src/outbox/index.ts +84 -19
  311. package/src/ports/audit.ts +120 -11
  312. package/src/ports/auth.ts +132 -0
  313. package/src/ports/events.ts +2 -2
  314. package/src/ports/index.ts +104 -35
  315. package/src/ports/policy.ts +50 -3
  316. package/src/ports/testing.ts +2220 -33
  317. package/src/ports/unbound.ts +64 -0
  318. package/src/ports/unit-of-work.ts +6 -2
  319. package/src/providers/index.ts +16 -3
  320. package/src/providers/instrumentation.ts +86 -7
  321. package/src/providers/metadata.ts +234 -0
  322. package/src/providers/provider.ts +168 -9
  323. package/src/schedules/index.ts +173 -23
  324. package/src/server/audit-context.ts +45 -0
  325. package/src/server/context.ts +224 -0
  326. package/src/server/contract-like.ts +1 -1
  327. package/src/server/health.ts +2 -2
  328. package/src/server/hooks/auth.ts +141 -51
  329. package/src/server/hooks/cors.ts +1 -1
  330. package/src/server/hooks/errors.ts +7 -4
  331. package/src/server/hooks/idempotency.ts +263 -0
  332. package/src/server/hooks/index.ts +14 -7
  333. package/src/server/hooks/logging.ts +3 -3
  334. package/src/server/hooks/rate-limit.ts +85 -17
  335. package/src/server/hooks.ts +1 -1
  336. package/src/server/http.ts +78 -51
  337. package/src/server/index.ts +62 -12
  338. package/src/server/instrumentation.ts +470 -0
  339. package/src/server/openapi.ts +4 -4
  340. package/src/server/providers/index.ts +6 -3
  341. package/src/server/providers/loadProviderConfig.ts +4 -4
  342. package/src/server/request-context.ts +116 -0
  343. package/src/server/server-context.ts +44 -0
  344. package/src/server/server.ts +886 -238
  345. package/src/server/types.ts +2 -2
  346. package/src/server/use-case-route.ts +430 -0
  347. package/src/server-only.ts +7 -0
  348. package/src/tasks/index.ts +275 -0
  349. package/src/testing/index.ts +1142 -6
  350. package/src/tracing/index.ts +176 -0
  351. package/src/uploads/client.ts +1 -1
  352. package/src/uploads/index.ts +7 -3
  353. package/dist/ports/mailer.d.ts +0 -6
  354. package/dist/ports/mailer.d.ts.map +0 -1
  355. package/dist/ports/mailer.js +0 -2
  356. package/dist/ports/mailer.js.map +0 -1
  357. package/dist/ports/schedules.d.ts +0 -9
  358. package/dist/ports/schedules.d.ts.map +0 -1
  359. package/dist/ports/schedules.js +0 -2
  360. package/dist/ports/schedules.js.map +0 -1
@@ -1,14 +1,990 @@
1
- import { type ClockPort, createSystemClock } from "../ports/clock";
1
+ import {
2
+ createMemoryIdempotencyStore,
3
+ type MemoryIdempotencyStore,
4
+ } from "../idempotency/index.js";
5
+ import { createMemoryMailer, type MemoryMailerPort } from "../mail/index.js";
6
+ import {
7
+ createMemoryNotificationPort,
8
+ type MemoryNotificationPort,
9
+ } from "../notifications/index.js";
10
+ import { createMemoryOutbox, type MemoryOutboxPort } from "../outbox/index.js";
11
+ import {
12
+ type ActivityActor,
13
+ type ActivityTenant,
14
+ type AuditLogPort,
15
+ createMemoryAuditLog,
16
+ type MemoryAuditLogPort,
17
+ } from "../ports/audit.js";
18
+ import { type CachePort, createMemoryCache } from "../ports/cache.js";
19
+ import {
20
+ type ClockPort,
21
+ createFrozenClock,
22
+ createSystemClock,
23
+ } from "../ports/clock.js";
24
+ import type { EventBusPort, JobDispatcherPort } from "../ports/events.js";
2
25
  import {
3
26
  createUuidIdGenerator,
4
27
  type IdGeneratorPort,
5
- } from "../ports/id-generator";
28
+ } from "../ports/id-generator.js";
29
+ import {
30
+ type AnyPorts,
31
+ type BoundGate,
32
+ type BufferedDomainEventRecorder,
33
+ createGate,
34
+ createMemoryRateLimiter,
35
+ createMemoryStorage,
36
+ createNoopLogger,
37
+ createNoopUnitOfWork,
38
+ type GatePort,
39
+ type LoggerPort,
40
+ type RateLimitPort,
41
+ type StoragePort,
42
+ type UnitOfWorkPort,
43
+ } from "../ports/index.js";
44
+ import {
45
+ createRecordingEventBus,
46
+ createRecordingJobDispatcher,
47
+ createTestActivityContext,
48
+ createTestSystemActor,
49
+ createTestUserActor,
50
+ type RecordedEvent,
51
+ type RecordedJobDispatch,
52
+ } from "../ports/testing.js";
53
+ import type {
54
+ AnyServiceProvider,
55
+ ProviderSetupResult,
56
+ } from "../providers/provider.js";
57
+ import { createAmbientAuditLog } from "../server/audit-context.js";
58
+ import {
59
+ clearActiveRequestContext,
60
+ enterActiveRequestContext,
61
+ } from "../server/request-context.js";
6
62
 
7
63
  /**
8
64
  * Value that may be returned synchronously or asynchronously.
9
65
  */
10
66
  export type MaybePromise<T> = T | Promise<T>;
11
67
 
68
+ /**
69
+ * Common Beignet ports created by `createTestPorts(...)`.
70
+ */
71
+ export interface CommonTestPorts<TxPorts = AnyPorts> {
72
+ [key: string]: unknown;
73
+ /**
74
+ * Ambient-enriched in-memory audit log. Entries recorded through this port
75
+ * inherit actor, tenant, request ID, and trace ID from the active request
76
+ * context, matching production server behavior. Use the fixture's `audit`
77
+ * for entry assertions.
78
+ */
79
+ audit: AuditLogPort;
80
+ /**
81
+ * In-memory string cache.
82
+ */
83
+ cache: CachePort;
84
+ /**
85
+ * Mutable deterministic test clock.
86
+ */
87
+ clock: ClockPort;
88
+ /**
89
+ * Recording event bus.
90
+ */
91
+ eventBus: EventBusPort;
92
+ /**
93
+ * Authorization gate. Apps with real policies should provide their own gate
94
+ * through `overrides`.
95
+ */
96
+ gate: GatePort<unknown, []>;
97
+ /**
98
+ * In-memory idempotency store.
99
+ */
100
+ idempotency: MemoryIdempotencyStore;
101
+ /**
102
+ * Sequence or UUID generator used by tests.
103
+ */
104
+ ids: IdGeneratorPort;
105
+ /**
106
+ * Recording job dispatcher.
107
+ */
108
+ jobs: JobDispatcherPort;
109
+ /**
110
+ * No-op logger.
111
+ */
112
+ logger: LoggerPort;
113
+ /**
114
+ * In-memory mailer.
115
+ */
116
+ mailer: MemoryMailerPort;
117
+ /**
118
+ * In-memory notification port.
119
+ */
120
+ notifications: MemoryNotificationPort;
121
+ /**
122
+ * In-memory outbox.
123
+ */
124
+ outbox: MemoryOutboxPort;
125
+ /**
126
+ * In-memory rate limiter.
127
+ */
128
+ rateLimit: RateLimitPort;
129
+ /**
130
+ * In-memory object storage.
131
+ */
132
+ storage: StoragePort;
133
+ /**
134
+ * No-op Unit of Work over the configured transaction ports.
135
+ */
136
+ uow: UnitOfWorkPort<TxPorts>;
137
+ }
138
+
139
+ /**
140
+ * Captured state and ports returned by `createTestPorts(...)`.
141
+ */
142
+ export interface TestPortsFixture<
143
+ Ports extends AnyPorts = CommonTestPorts,
144
+ TxPorts = Ports,
145
+ > {
146
+ /**
147
+ * App ports to pass into a use-case or route test context.
148
+ */
149
+ ports: Ports;
150
+ /**
151
+ * Memory audit port, exposed for assertions.
152
+ */
153
+ audit: MemoryAuditLogPort;
154
+ /**
155
+ * Recording event bus port.
156
+ */
157
+ eventBus: EventBusPort;
158
+ /**
159
+ * Recorded events published through `eventBus`.
160
+ */
161
+ events: RecordedEvent[];
162
+ /**
163
+ * Recording job dispatcher port.
164
+ */
165
+ jobs: JobDispatcherPort;
166
+ /**
167
+ * Jobs dispatched through `jobs`.
168
+ */
169
+ dispatchedJobs: RecordedJobDispatch[];
170
+ /**
171
+ * Memory mailer, exposed for delivery assertions.
172
+ */
173
+ mailer: MemoryMailerPort;
174
+ /**
175
+ * Memory notification port, exposed for delivery assertions.
176
+ */
177
+ notifications: MemoryNotificationPort;
178
+ /**
179
+ * Memory outbox, exposed for durable workflow assertions.
180
+ */
181
+ outbox: MemoryOutboxPort;
182
+ /**
183
+ * Memory storage port.
184
+ */
185
+ storage: StoragePort;
186
+ /**
187
+ * Memory idempotency store.
188
+ */
189
+ idempotency: MemoryIdempotencyStore;
190
+ /**
191
+ * Test cache port.
192
+ */
193
+ cache: CachePort;
194
+ /**
195
+ * Test rate-limit port.
196
+ */
197
+ rateLimit: RateLimitPort;
198
+ /**
199
+ * Test logger port.
200
+ */
201
+ logger: LoggerPort;
202
+ /**
203
+ * Test clock port.
204
+ */
205
+ clock: ClockPort;
206
+ /**
207
+ * Test ID generator.
208
+ */
209
+ ids: IdGeneratorPort;
210
+ /**
211
+ * Unit of Work port installed on `ports`.
212
+ */
213
+ uow: UnitOfWorkPort<TxPorts>;
214
+ }
215
+
216
+ /**
217
+ * Unit of Work behavior for `createTestPorts(...)`.
218
+ */
219
+ export interface CreateTestPortsTransactionOptions<
220
+ Ports extends AnyPorts,
221
+ TxPorts,
222
+ > {
223
+ /**
224
+ * Transaction-scoped ports or a function that derives them from final ports.
225
+ *
226
+ * Defaults to the final `ports` object.
227
+ */
228
+ ports?: TxPorts | ((ports: Ports) => TxPorts);
229
+ /**
230
+ * Hook run after the transaction callback resolves.
231
+ */
232
+ afterCommit?: (tx: TxPorts) => MaybePromise<void>;
233
+ /**
234
+ * Hook run after the transaction callback throws.
235
+ */
236
+ afterRollback?: (error: unknown, tx: TxPorts) => MaybePromise<void>;
237
+ /**
238
+ * Flush `tx.events` (a buffered domain event recorder) to `ports.eventBus`
239
+ * after the transaction commits, and clear it after a rollback.
240
+ *
241
+ * Requires `transaction.ports` to include an `events` recorder such as
242
+ * `createDomainEventRecorder()` or `createOutboxEventRecorder(...)`.
243
+ */
244
+ outbox?: boolean;
245
+ }
246
+
247
+ /**
248
+ * Override shape for one test port.
249
+ *
250
+ * Object-valued ports may be supplied one level deep as partials: the missing
251
+ * members are completed behind a proxy that throws a named error on use.
252
+ * Function-valued ports must be supplied whole, so the check for functions
253
+ * happens before the object branch.
254
+ */
255
+ export type TestPortOverride<Port> = Port extends (...args: never[]) => unknown
256
+ ? Port
257
+ : Port extends object
258
+ ? Partial<Port>
259
+ : Port;
260
+
261
+ /**
262
+ * Typed partial port overrides accepted by `createTestPorts(...)` and
263
+ * `createTestContext(...)`.
264
+ */
265
+ export type TestPortsOverrides<Ports extends AnyPorts> = {
266
+ [K in keyof Ports]?: TestPortOverride<Ports[K]>;
267
+ };
268
+
269
+ /**
270
+ * Options for `createTestPorts(...)`.
271
+ */
272
+ export interface CreateTestPortsOptions<
273
+ Ports extends AnyPorts,
274
+ TxPorts = Ports,
275
+ > {
276
+ /**
277
+ * App-owned default ports, usually imported from `infra/app-ports`. Common
278
+ * Beignet test defaults replace matching keys from `base`; use `overrides`
279
+ * for app ports that should win.
280
+ *
281
+ * Values are intentionally loose: boot-time app ports may bind wider types
282
+ * (or deferred placeholders) than the provider-contributed runtime ports.
283
+ */
284
+ base?: { [K in keyof Ports]?: unknown };
285
+ /**
286
+ * Test-specific ports that should replace generated defaults or `base`.
287
+ *
288
+ * Plain-object ports may be supplied as one-level-deep partials; missing
289
+ * members throw a named error when called. Function-valued ports, class
290
+ * instances, and other exotic objects are passed through whole.
291
+ */
292
+ overrides?: TestPortsOverrides<Ports>;
293
+ /**
294
+ * Clock implementation. Defaults to a frozen clock at the Unix epoch.
295
+ */
296
+ clock?: ClockPort;
297
+ /**
298
+ * ID generator. Defaults to UUIDs.
299
+ */
300
+ ids?: IdGeneratorPort;
301
+ /**
302
+ * Unit of Work configuration. A no-op UOW is installed by default.
303
+ */
304
+ transaction?: CreateTestPortsTransactionOptions<Ports, TxPorts>;
305
+ }
306
+
307
+ /**
308
+ * Create standard memory/fake Beignet ports for tests.
309
+ *
310
+ * Use this as the starting point for use-case and route tests, then layer
311
+ * app-owned repositories or provider fakes through `base` and `overrides`.
312
+ *
313
+ * @param options - Optional app ports, overrides, and Unit of Work behavior.
314
+ * @returns Ports plus captured state for assertions.
315
+ */
316
+ export function createTestPorts<
317
+ Ports extends AnyPorts = CommonTestPorts,
318
+ TxPorts = Ports,
319
+ >(
320
+ options: CreateTestPortsOptions<Ports, TxPorts> = {},
321
+ ): TestPortsFixture<Ports, TxPorts> {
322
+ const audit = createMemoryAuditLog();
323
+ const cache = createMemoryCache();
324
+ const clock = options.clock ?? createFrozenClock();
325
+ const { bus: eventBus, events } = createRecordingEventBus();
326
+ const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
327
+ const idempotency = createMemoryIdempotencyStore();
328
+ const ids = options.ids ?? createUuidIdGenerator();
329
+ const logger = createNoopLogger();
330
+ const mailer = createMemoryMailer();
331
+ const notifications = createMemoryNotificationPort();
332
+ const outbox = createMemoryOutbox();
333
+ const rateLimit = createMemoryRateLimiter();
334
+ const storage = createMemoryStorage();
335
+
336
+ const defaultPorts = {
337
+ audit: createAmbientAuditLog(audit),
338
+ cache,
339
+ clock,
340
+ eventBus,
341
+ gate: createGate({ policies: [] }),
342
+ idempotency,
343
+ ids,
344
+ jobs,
345
+ logger,
346
+ mailer,
347
+ notifications,
348
+ outbox,
349
+ rateLimit,
350
+ storage,
351
+ };
352
+ const overrides = completeTestPortOverrides(options.overrides);
353
+ // The kit's one sanctioned cast: boot-time bases and completed partial
354
+ // overrides are widened to the app's full Ports shape.
355
+ const portsWithoutUow = {
356
+ ...options.base,
357
+ ...defaultPorts,
358
+ ...overrides,
359
+ } as unknown as Ports;
360
+ const transactionOptions = options.transaction;
361
+ const flushEventsToOutbox = transactionOptions?.outbox === true;
362
+ const resolveTransactionPorts = (): TxPorts => {
363
+ const txPorts = transactionOptions?.ports;
364
+ if (typeof txPorts === "function") {
365
+ return (txPorts as (ports: Ports) => TxPorts)(portsWithoutUow);
366
+ }
367
+
368
+ return txPorts ?? (portsWithoutUow as unknown as TxPorts);
369
+ };
370
+ const generatedUow = createNoopUnitOfWork<TxPorts>(
371
+ () => {
372
+ const tx = resolveTransactionPorts();
373
+ if (flushEventsToOutbox) {
374
+ // Validate up front so both commit and rollback paths fail loudly.
375
+ resolveBufferedTransactionEvents(tx);
376
+ }
377
+
378
+ return tx;
379
+ },
380
+ {
381
+ afterCommit: async (tx) => {
382
+ if (flushEventsToOutbox) {
383
+ await resolveBufferedTransactionEvents(tx).flush(
384
+ (portsWithoutUow as unknown as { eventBus: EventBusPort }).eventBus,
385
+ );
386
+ }
387
+ await transactionOptions?.afterCommit?.(tx);
388
+ },
389
+ afterRollback: async (error, tx) => {
390
+ if (flushEventsToOutbox) {
391
+ resolveBufferedTransactionEvents(tx).clear();
392
+ }
393
+ await transactionOptions?.afterRollback?.(error, tx);
394
+ },
395
+ },
396
+ );
397
+ // The kit consumes `uow` directly, so keep the supplied port's identity
398
+ // instead of the completed override.
399
+ const uow =
400
+ (options.overrides as { uow?: UnitOfWorkPort<TxPorts> } | undefined)?.uow ??
401
+ generatedUow;
402
+ const ports = {
403
+ ...portsWithoutUow,
404
+ uow,
405
+ } as Ports;
406
+
407
+ return {
408
+ ports,
409
+ audit,
410
+ eventBus,
411
+ events,
412
+ jobs,
413
+ dispatchedJobs,
414
+ mailer,
415
+ notifications,
416
+ outbox,
417
+ storage,
418
+ idempotency,
419
+ cache,
420
+ rateLimit,
421
+ logger,
422
+ clock,
423
+ ids,
424
+ uow,
425
+ };
426
+ }
427
+
428
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
429
+ if (value === null || typeof value !== "object") return false;
430
+ const proto = Object.getPrototypeOf(value);
431
+
432
+ return proto === Object.prototype || proto === null;
433
+ }
434
+
435
+ function createMissingTestPortMember(
436
+ portKey: string,
437
+ member: string,
438
+ ): () => never {
439
+ const memberName = `${portKey}.${member}`;
440
+ const throwMissingMember = () => {
441
+ throw new Error(
442
+ `Test port "${memberName}" was called but not provided. Pass it through createTestPorts overrides.`,
443
+ );
444
+ };
445
+ Object.defineProperty(throwMissingMember, "name", {
446
+ value: memberName,
447
+ configurable: true,
448
+ });
449
+
450
+ return throwMissingMember;
451
+ }
452
+
453
+ function completeTestPortOverride(portKey: string, value: unknown): unknown {
454
+ // Functions are objects: the function check must run first so
455
+ // function-valued ports pass through whole.
456
+ if (typeof value === "function") return value;
457
+ // Class instances (repositories, Maps, gates built from classes) rely on
458
+ // internal slots or prototype identity, so only plain objects are completed.
459
+ if (!isPlainObject(value)) return value;
460
+
461
+ return new Proxy(value, {
462
+ get(target, member, receiver) {
463
+ if (
464
+ typeof member === "symbol" ||
465
+ member === "then" ||
466
+ member === "constructor" ||
467
+ member in target
468
+ ) {
469
+ return Reflect.get(target, member, receiver);
470
+ }
471
+
472
+ return createMissingTestPortMember(portKey, member);
473
+ },
474
+ });
475
+ }
476
+
477
+ function completeTestPortOverrides<Ports extends AnyPorts>(
478
+ overrides: TestPortsOverrides<Ports> | undefined,
479
+ ): Record<string, unknown> {
480
+ const completed: Record<string, unknown> = {};
481
+ for (const [key, value] of Object.entries(overrides ?? {})) {
482
+ if (value === undefined) continue;
483
+ completed[key] = completeTestPortOverride(key, value);
484
+ }
485
+
486
+ return completed;
487
+ }
488
+
489
+ function resolveBufferedTransactionEvents(
490
+ tx: unknown,
491
+ ): Pick<BufferedDomainEventRecorder, "flush" | "clear"> {
492
+ const events =
493
+ tx && typeof tx === "object"
494
+ ? (tx as { events?: unknown }).events
495
+ : undefined;
496
+ const recorder = events as
497
+ | Partial<Pick<BufferedDomainEventRecorder, "flush" | "clear">>
498
+ | undefined;
499
+ if (
500
+ !recorder ||
501
+ typeof recorder.flush !== "function" ||
502
+ typeof recorder.clear !== "function"
503
+ ) {
504
+ throw new Error(
505
+ "createTestPorts transaction.outbox requires tx.events to be a buffered domain event recorder. " +
506
+ "Add events: createDomainEventRecorder() or createOutboxEventRecorder(...) to transaction.ports.",
507
+ );
508
+ }
509
+
510
+ return recorder as Pick<BufferedDomainEventRecorder, "flush" | "clear">;
511
+ }
512
+
513
+ /**
514
+ * Bound gate type inferred from a ports object.
515
+ */
516
+ export type TestBoundGateForPorts<Ports extends AnyPorts> = Ports extends {
517
+ gate?: { bind: (ctx: never) => infer Bound };
518
+ }
519
+ ? Bound
520
+ : BoundGate<readonly []>;
521
+
522
+ /**
523
+ * Minimal gate shape used by `createTestContextFactory(...)`.
524
+ */
525
+ type TestGatePort<BoundGateValue = BoundGate<readonly []>> = {
526
+ /**
527
+ * Bind the gate to a context.
528
+ */
529
+ bind: (ctx: never) => BoundGateValue;
530
+ };
531
+
532
+ /**
533
+ * Common app context fields built by `createTestContextFactory(...)`.
534
+ */
535
+ export type TestContextFields<Ports extends AnyPorts> = {
536
+ /**
537
+ * Actor under test.
538
+ */
539
+ actor: ActivityActor;
540
+ /**
541
+ * Optional tenant under test.
542
+ */
543
+ tenant?: ActivityTenant;
544
+ /**
545
+ * Stable request ID.
546
+ */
547
+ requestId: string;
548
+ /**
549
+ * Stable trace ID.
550
+ */
551
+ traceId?: string;
552
+ /**
553
+ * Optional auth/session value.
554
+ */
555
+ auth?: unknown;
556
+ /**
557
+ * App ports under test.
558
+ */
559
+ ports: Ports;
560
+ /**
561
+ * Bound authorization gate when `ports.gate` is available.
562
+ */
563
+ gate?: TestBoundGateForPorts<Ports>;
564
+ };
565
+
566
+ /**
567
+ * Options for `createTestContextFactory(...)`.
568
+ */
569
+ export interface CreateTestContextFactoryOptions<Ctx, Ports extends AnyPorts> {
570
+ /**
571
+ * Ports or a callback returning ports for each created context.
572
+ */
573
+ ports: Ports | (() => Ports);
574
+ /**
575
+ * Actor used by default.
576
+ *
577
+ * @default createTestUserActor()
578
+ */
579
+ actor?: ActivityActor;
580
+ /**
581
+ * Tenant used by default. Pass `null` to omit tenant context.
582
+ */
583
+ tenant?: ActivityTenant | null;
584
+ /**
585
+ * Request ID used by default.
586
+ */
587
+ requestId?: string;
588
+ /**
589
+ * Trace ID used by default.
590
+ */
591
+ traceId?: string;
592
+ /**
593
+ * Auth/session value added to the context.
594
+ */
595
+ auth?: unknown;
596
+ /**
597
+ * Additional app-specific context fields.
598
+ */
599
+ extra?: Partial<Ctx> | ((args: TestContextFields<Ports>) => Partial<Ctx>);
600
+ }
601
+
602
+ /**
603
+ * Per-call overrides for a test context factory.
604
+ */
605
+ export type TestContextFactoryOverrides<Ctx, Ports extends AnyPorts> = Partial<
606
+ Pick<
607
+ CreateTestContextFactoryOptions<Ctx, Ports>,
608
+ "actor" | "tenant" | "requestId" | "traceId" | "auth" | "extra"
609
+ >
610
+ > & {
611
+ /**
612
+ * Ports for this context call.
613
+ */
614
+ ports?: Ports;
615
+ };
616
+
617
+ /**
618
+ * Create a repeatable app context factory for use-case and route tests.
619
+ *
620
+ * The factory adds actor, tenant, request ID, trace ID, ports, and auth fields,
621
+ * and attaches a live `ctx.gate` automatically when `ports.gate` exposes
622
+ * `bind(...)`. The gate is attached after all `extra` and override fields are
623
+ * merged, so it always authorizes against the final context identity.
624
+ *
625
+ * @param options - Default context values.
626
+ * @returns A function that creates one app context.
627
+ */
628
+ export function createTestContextFactory<
629
+ Ctx extends object,
630
+ Ports extends AnyPorts,
631
+ >(
632
+ options: CreateTestContextFactoryOptions<Ctx, Ports>,
633
+ ): (overrides?: TestContextFactoryOverrides<Ctx, Ports>) => Ctx {
634
+ return (overrides = {}) => {
635
+ const ports = overrides.ports ?? resolvePorts(options.ports);
636
+ const activity = createTestActivityContext({
637
+ actor: overrides.actor ?? options.actor ?? createTestUserActor(),
638
+ tenant:
639
+ overrides.tenant !== undefined ? overrides.tenant : options.tenant,
640
+ requestId: overrides.requestId ?? options.requestId,
641
+ traceId: overrides.traceId ?? options.traceId,
642
+ });
643
+ const auth =
644
+ overrides.auth !== undefined ? overrides.auth : (options.auth ?? null);
645
+ const base = {
646
+ ...activity,
647
+ auth,
648
+ ports,
649
+ } as TestContextFields<Ports>;
650
+ const fields = attachTestGate(ports, base);
651
+ const extra = resolveExtra(options.extra, fields);
652
+ const overrideExtra = resolveExtra(overrides.extra, fields);
653
+ const merged = {
654
+ ...fields,
655
+ ...extra,
656
+ ...overrideExtra,
657
+ };
658
+
659
+ if (Object.hasOwn(merged, "gate")) {
660
+ return merged as Ctx;
661
+ }
662
+
663
+ return attachTestGate(ports, merged) as Ctx;
664
+ };
665
+ }
666
+
667
+ function resolvePorts<Ports extends AnyPorts>(
668
+ ports: Ports | (() => Ports),
669
+ ): Ports {
670
+ return typeof ports === "function" ? (ports as () => Ports)() : ports;
671
+ }
672
+
673
+ function resolveExtra<Ctx, Ports extends AnyPorts>(
674
+ extra:
675
+ | Partial<Ctx>
676
+ | ((args: TestContextFields<Ports>) => Partial<Ctx>)
677
+ | undefined,
678
+ fields: TestContextFields<Ports>,
679
+ ): Partial<Ctx> {
680
+ return typeof extra === "function" ? extra(fields) : (extra ?? {});
681
+ }
682
+
683
+ function attachTestGate<Ports extends AnyPorts, Ctx extends object>(
684
+ ports: Ports,
685
+ ctx: Ctx,
686
+ ): Ctx {
687
+ const maybeGate = (
688
+ ports as { gate?: TestGatePort<TestBoundGateForPorts<Ports>> }
689
+ ).gate;
690
+ if (!maybeGate) return ctx;
691
+
692
+ // Mirror GatePort.attach: a live, non-enumerable getter that re-binds
693
+ // against the receiver so in-place identity changes are never stale and
694
+ // spread copies drop the gate loudly.
695
+ Object.defineProperty(ctx, "gate", {
696
+ configurable: true,
697
+ enumerable: false,
698
+ get() {
699
+ return maybeGate.bind(this as never);
700
+ },
701
+ });
702
+
703
+ return ctx;
704
+ }
705
+
706
+ const disposeSymbol: typeof Symbol.dispose =
707
+ Symbol.dispose ?? (Symbol.for("Symbol.dispose") as typeof Symbol.dispose);
708
+
709
+ /**
710
+ * Options for `createTestContext(...)`.
711
+ */
712
+ export interface CreateTestContextOptions<Ctx, Ports extends AnyPorts> {
713
+ /**
714
+ * Actor under test.
715
+ *
716
+ * @default createTestSystemActor("test-system")
717
+ */
718
+ actor?: ActivityActor;
719
+ /**
720
+ * Tenant under test. Pass `null` to omit tenant context.
721
+ *
722
+ * @default createTestTenant()
723
+ */
724
+ tenant?: ActivityTenant | null;
725
+ /**
726
+ * Request ID exposed on the context.
727
+ *
728
+ * @default "test-request"
729
+ */
730
+ requestId?: string;
731
+ /**
732
+ * Trace ID exposed on the context.
733
+ *
734
+ * @default "test-trace"
735
+ */
736
+ traceId?: string;
737
+ /**
738
+ * Auth/session value added to the context.
739
+ *
740
+ * @default null
741
+ */
742
+ auth?: unknown;
743
+ /**
744
+ * App-owned default ports, usually imported from `infra/app-ports`. Common
745
+ * Beignet test defaults replace matching keys from `base`; use `ports` for
746
+ * app ports that should win.
747
+ */
748
+ base?: { [K in keyof Ports]?: unknown };
749
+ /**
750
+ * Typed partial port overrides that replace generated defaults or `base`.
751
+ */
752
+ ports?: TestPortsOverrides<Ports>;
753
+ /**
754
+ * Clock implementation. Defaults to a frozen clock at the Unix epoch.
755
+ */
756
+ clock?: ClockPort;
757
+ /**
758
+ * ID generator. Defaults to UUIDs.
759
+ */
760
+ ids?: IdGeneratorPort;
761
+ /**
762
+ * Unit of Work configuration. A no-op UOW is installed by default.
763
+ */
764
+ transaction?: CreateTestPortsTransactionOptions<Ports, unknown>;
765
+ /**
766
+ * Additional app-specific context fields. An explicit `gate` here wins over
767
+ * the kit-attached gate.
768
+ */
769
+ extra?: Partial<Ctx>;
770
+ /**
771
+ * Enter the ambient request context with the test actor, tenant, request
772
+ * ID, and trace ID so ambient enrichment (such as the default audit port)
773
+ * works like production. Call `dispose()` (or use `using`) to clear it.
774
+ *
775
+ * @default true
776
+ */
777
+ ambient?: boolean;
778
+ }
779
+
780
+ /**
781
+ * Test context fixture returned by `createTestContext(...)`.
782
+ */
783
+ export interface TestContextFixture<Ctx, Ports extends AnyPorts>
784
+ extends TestPortsFixture<Ports, unknown> {
785
+ /**
786
+ * Assembled app context under test.
787
+ */
788
+ ctx: Ctx;
789
+ /**
790
+ * Clear the ambient request context entered by this fixture.
791
+ */
792
+ dispose(): void;
793
+ /**
794
+ * Explicit-resource-management alias for `dispose()`, so fixtures work with
795
+ * `using`.
796
+ */
797
+ [disposeSymbol](): void;
798
+ }
799
+
800
+ /**
801
+ * Create a one-call test context fixture for jobs, listeners, schedules,
802
+ * notifications, tasks, and use-case tests.
803
+ *
804
+ * The fixture builds common memory ports through `createTestPorts(...)`,
805
+ * assembles an app context with actor, tenant, request ID, trace ID, auth,
806
+ * and a live bound gate, and enters the ambient request context so ambient
807
+ * enrichment matches production. Reading an app port that is neither a kit
808
+ * default nor supplied throws a named error on use.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * const makeContext = createTestContext<AppContext>();
813
+ *
814
+ * using fixture = makeContext({
815
+ * ports: { issues: { create: async (input) => issueRecord(input) } },
816
+ * });
817
+ * await runCreateIssue(fixture.ctx);
818
+ * ```
819
+ *
820
+ * @returns A factory that creates one disposable test context fixture.
821
+ */
822
+ export function createTestContext<Ctx extends { ports: AnyPorts }>(): (
823
+ options?: CreateTestContextOptions<Ctx, Ctx["ports"]>,
824
+ ) => TestContextFixture<Ctx, Ctx["ports"]> {
825
+ return (options = {}) => {
826
+ const fixture = createTestPorts<Ctx["ports"], unknown>({
827
+ base: options.base,
828
+ overrides: options.ports,
829
+ clock: options.clock,
830
+ ids: options.ids,
831
+ transaction: options.transaction,
832
+ });
833
+ const ports = createUnboundPortGuard(fixture.ports);
834
+ const activity = createTestActivityContext({
835
+ actor: options.actor ?? createTestSystemActor("test-system"),
836
+ tenant: options.tenant,
837
+ requestId: options.requestId,
838
+ traceId: options.traceId,
839
+ });
840
+ const auth = options.auth !== undefined ? options.auth : null;
841
+ const base = {
842
+ ...activity,
843
+ auth,
844
+ ports,
845
+ } as TestContextFields<Ctx["ports"]>;
846
+ const fields = attachTestGate(ports, base);
847
+ const merged = {
848
+ ...fields,
849
+ ...(options.extra ?? {}),
850
+ };
851
+ const ctx = (Object.hasOwn(merged, "gate")
852
+ ? merged
853
+ : attachTestGate(ports, merged)) as unknown as Ctx;
854
+
855
+ const ambient = options.ambient ?? true;
856
+ if (ambient) {
857
+ enterActiveRequestContext({
858
+ requestId: activity.requestId,
859
+ traceId: activity.traceId,
860
+ actor: activity.actor,
861
+ tenant: activity.tenant,
862
+ });
863
+ }
864
+ let disposed = false;
865
+ const dispose = () => {
866
+ if (!ambient || disposed) return;
867
+ disposed = true;
868
+ clearActiveRequestContext();
869
+ };
870
+
871
+ return {
872
+ ...fixture,
873
+ ports,
874
+ ctx,
875
+ dispose,
876
+ [disposeSymbol]: dispose,
877
+ };
878
+ };
879
+ }
880
+
881
+ function createUnboundPortGuard<Ports extends AnyPorts>(ports: Ports): Ports {
882
+ return new Proxy(ports, {
883
+ get(target, key, receiver) {
884
+ if (
885
+ typeof key === "symbol" ||
886
+ key === "then" ||
887
+ key === "constructor" ||
888
+ key in target
889
+ ) {
890
+ return Reflect.get(target, key, receiver);
891
+ }
892
+
893
+ throw new Error(
894
+ `App port "${key}" is not bound in this test context. Pass it through createTestContext ports.`,
895
+ );
896
+ },
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Options for `installProviderForTest(...)`.
902
+ */
903
+ export interface InstallProviderForTestOptions {
904
+ /**
905
+ * Base app ports visible to provider setup, such as `{ devtools }`.
906
+ */
907
+ ports?: Record<string, unknown>;
908
+ /**
909
+ * Config passed to provider setup as-is. The provider config schema is not
910
+ * run, matching server startup where config is validated before setup.
911
+ */
912
+ config?: unknown;
913
+ /**
914
+ * Service-context factory passed to setup and lifecycle hooks. The default
915
+ * factory rejects, mirroring the late-bound runtime factory before all
916
+ * providers have started.
917
+ */
918
+ createServiceContext?: (input?: unknown) => Promise<unknown>;
919
+ }
920
+
921
+ /**
922
+ * Installed provider fixture returned by `installProviderForTest(...)`.
923
+ */
924
+ export interface InstalledProviderFixture {
925
+ /**
926
+ * Base ports merged with the ports contributed by provider setup.
927
+ */
928
+ ports: Record<string, unknown>;
929
+ /**
930
+ * Raw provider setup result, exposed for assertions on contributed ports
931
+ * and optional lifecycle hooks.
932
+ */
933
+ result: ProviderSetupResult<Record<string, unknown>, Record<string, unknown>>;
934
+ /**
935
+ * Run the provider `start` hook when the provider declares one.
936
+ */
937
+ start(): Promise<void>;
938
+ /**
939
+ * Run the provider `stop` hook when the provider declares one.
940
+ */
941
+ stop(): Promise<void>;
942
+ }
943
+
944
+ /**
945
+ * Run provider setup against test ports and return the merged ports plus
946
+ * lifecycle runners.
947
+ *
948
+ * Use this in provider tests instead of hand-rolling the setup, port merge,
949
+ * and lifecycle plumbing around `provider.setup(...)`.
950
+ *
951
+ * @param provider - Provider under test.
952
+ * @param options - Base ports, raw config, and service-context factory.
953
+ * @returns Merged ports, the raw setup result, and start/stop runners.
954
+ */
955
+ export async function installProviderForTest(
956
+ provider: AnyServiceProvider,
957
+ options: InstallProviderForTestOptions = {},
958
+ ): Promise<InstalledProviderFixture> {
959
+ const basePorts: Record<string, unknown> = { ...options.ports };
960
+ const createServiceContext =
961
+ options.createServiceContext ??
962
+ (async () => {
963
+ throw new Error(
964
+ `Provider "${provider.name}" called createServiceContext during a test install. ` +
965
+ "Pass createServiceContext to installProviderForTest(...) when the test needs a service context.",
966
+ );
967
+ });
968
+ const result = await provider.setup({
969
+ ports: basePorts,
970
+ config: options.config,
971
+ createServiceContext,
972
+ });
973
+ const ports: Record<string, unknown> = { ...basePorts, ...result.ports };
974
+ const lifecycleCtx = { ports, createServiceContext };
975
+
976
+ return {
977
+ ports,
978
+ result,
979
+ async start() {
980
+ await result.start?.(lifecycleCtx);
981
+ },
982
+ async stop() {
983
+ await result.stop?.(lifecycleCtx);
984
+ },
985
+ };
986
+ }
987
+
12
988
  /**
13
989
  * Arguments passed to a factory default builder.
14
990
  */
@@ -50,7 +1026,7 @@ export type FactoryOverrides<
50
1026
  /**
51
1027
  * Options for declaring a test data factory.
52
1028
  */
53
- export interface DefineFactoryOptions<
1029
+ export interface CreateFactoryOptions<
54
1030
  Name extends string,
55
1031
  Value extends object,
56
1032
  Ctx = unknown,
@@ -175,6 +1151,96 @@ export interface RunSeedsOptions<Ctx> {
175
1151
  seeds: readonly SeedDef<Ctx>[];
176
1152
  }
177
1153
 
1154
+ /**
1155
+ * Options for creating a database test harness.
1156
+ */
1157
+ export interface CreateDatabaseTestHarnessOptions<Database, Ctx> {
1158
+ /**
1159
+ * Create an isolated app-owned database fixture.
1160
+ */
1161
+ create(): MaybePromise<Database>;
1162
+ /**
1163
+ * Build the context passed to factories and seeds from the database fixture.
1164
+ */
1165
+ ctx(database: Database): Ctx;
1166
+ /**
1167
+ * Reset the database fixture, when supported by the app.
1168
+ */
1169
+ reset?(database: Database): MaybePromise<void>;
1170
+ /**
1171
+ * Close and dispose the database fixture.
1172
+ */
1173
+ close?(database: Database): MaybePromise<void>;
1174
+ /**
1175
+ * Factories whose sequences should reset before each setup and reset.
1176
+ */
1177
+ factories?: readonly Pick<FactoryDef, "resetSequence">[];
1178
+ /**
1179
+ * Default seeds available to `setup({ seed: true })` and `session.runSeeds()`.
1180
+ */
1181
+ seeds?: readonly SeedDef<Ctx>[];
1182
+ }
1183
+
1184
+ /**
1185
+ * Options for setting up one database test session.
1186
+ */
1187
+ export interface DatabaseTestSetupOptions<Ctx> {
1188
+ /**
1189
+ * Run seeds after creating the database session.
1190
+ *
1191
+ * Pass `true` to use the harness default seeds, or pass an explicit seed
1192
+ * list for this setup.
1193
+ */
1194
+ seed?: boolean | readonly SeedDef<Ctx>[];
1195
+ }
1196
+
1197
+ /**
1198
+ * Active database fixture created by `createDatabaseTestHarness(...)`.
1199
+ */
1200
+ export interface DatabaseTestSession<Database, Ctx> {
1201
+ /**
1202
+ * App-owned database fixture returned by the harness `create` function.
1203
+ */
1204
+ readonly database: Database;
1205
+ /**
1206
+ * Context built from the database fixture for factories and seeds.
1207
+ */
1208
+ readonly ctx: Ctx;
1209
+ /**
1210
+ * Run the default harness seeds or an explicit seed list.
1211
+ */
1212
+ runSeeds(seeds?: readonly SeedDef<Ctx>[]): Promise<void>;
1213
+ /**
1214
+ * Reset factory sequences and the database fixture.
1215
+ */
1216
+ reset(): Promise<void>;
1217
+ /**
1218
+ * Close and dispose this session.
1219
+ */
1220
+ close(): Promise<void>;
1221
+ }
1222
+
1223
+ /**
1224
+ * Database test harness that coordinates app-owned database fixtures with
1225
+ * Beignet factories and seeds.
1226
+ */
1227
+ export interface DatabaseTestHarness<Database, Ctx> {
1228
+ /**
1229
+ * Create one isolated database test session.
1230
+ */
1231
+ setup(
1232
+ options?: DatabaseTestSetupOptions<Ctx>,
1233
+ ): Promise<DatabaseTestSession<Database, Ctx>>;
1234
+ /**
1235
+ * Reset all configured factory sequences.
1236
+ */
1237
+ resetFactories(): void;
1238
+ /**
1239
+ * Close all sessions that have not already been closed.
1240
+ */
1241
+ cleanup(): Promise<void>;
1242
+ }
1243
+
178
1244
  /**
179
1245
  * Error thrown when a seed fails.
180
1246
  */
@@ -243,16 +1309,16 @@ function assertCount(kind: "buildList" | "createList", count: number): void {
243
1309
  }
244
1310
 
245
1311
  /**
246
- * Define a typed test data factory with deterministic sequence support.
1312
+ * Create a typed test data factory with deterministic sequence support.
247
1313
  */
248
- export function defineFactory<
1314
+ export function createFactory<
249
1315
  const Name extends string,
250
1316
  Value extends object,
251
1317
  Ctx = unknown,
252
1318
  Created = Value,
253
1319
  >(
254
1320
  name: Name,
255
- options: DefineFactoryOptions<Name, Value, Ctx, Created>,
1321
+ options: CreateFactoryOptions<Name, Value, Ctx, Created>,
256
1322
  ): FactoryDef<Name, Value, Ctx, Created> {
257
1323
  const start = options.start ?? 1;
258
1324
  const ids = options.ids ?? createUuidIdGenerator();
@@ -346,3 +1412,73 @@ export async function runSeeds<Ctx>(
346
1412
  }
347
1413
  }
348
1414
  }
1415
+
1416
+ /**
1417
+ * Create a database test harness for app-owned database fixtures.
1418
+ *
1419
+ * The harness does not know about an ORM or database provider. Apps provide
1420
+ * creation, reset, close, and context mapping functions, while Beignet handles
1421
+ * the repetitive test lifecycle around factory sequences and seed execution.
1422
+ */
1423
+ export function createDatabaseTestHarness<Database, Ctx>(
1424
+ options: CreateDatabaseTestHarnessOptions<Database, Ctx>,
1425
+ ): DatabaseTestHarness<Database, Ctx> {
1426
+ const sessions = new Set<DatabaseTestSession<Database, Ctx>>();
1427
+
1428
+ function resetFactorySequences(): void {
1429
+ resetFactories(...(options.factories ?? []));
1430
+ }
1431
+
1432
+ function defaultSeeds(): readonly SeedDef<Ctx>[] {
1433
+ return options.seeds ?? [];
1434
+ }
1435
+
1436
+ async function closeSession(
1437
+ session: DatabaseTestSession<Database, Ctx>,
1438
+ ): Promise<void> {
1439
+ if (!sessions.delete(session)) return;
1440
+ await options.close?.(session.database);
1441
+ }
1442
+
1443
+ return {
1444
+ async setup(setupOptions = {}) {
1445
+ resetFactorySequences();
1446
+
1447
+ const database = await options.create();
1448
+ const ctx = options.ctx(database);
1449
+ const session: DatabaseTestSession<Database, Ctx> = {
1450
+ database,
1451
+ ctx,
1452
+ async runSeeds(seeds = defaultSeeds()) {
1453
+ await runSeeds({ ctx, seeds });
1454
+ },
1455
+ async reset() {
1456
+ resetFactorySequences();
1457
+ await options.reset?.(database);
1458
+ },
1459
+ async close() {
1460
+ await closeSession(session);
1461
+ },
1462
+ };
1463
+
1464
+ sessions.add(session);
1465
+
1466
+ try {
1467
+ if (setupOptions.seed) {
1468
+ const seeds =
1469
+ setupOptions.seed === true ? defaultSeeds() : setupOptions.seed;
1470
+ await session.runSeeds(seeds);
1471
+ }
1472
+ } catch (error) {
1473
+ await session.close();
1474
+ throw error;
1475
+ }
1476
+
1477
+ return session;
1478
+ },
1479
+ resetFactories: resetFactorySequences,
1480
+ async cleanup() {
1481
+ await Promise.all(Array.from(sessions, (session) => session.close()));
1482
+ },
1483
+ };
1484
+ }