@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,323 @@
1
+ // Gold-standard (Marten) event sourcing: domain events emitted via
2
+ // ctx.appendEvent land on the same aggregate stream as the auto CRUD events,
3
+ // and inline projections fire off both. This test pins the end-to-end path:
4
+ //
5
+ // HTTP → Dispatcher → writeHandler → CRUD create (auto-event)
6
+ // → ctx.appendEvent (domain event)
7
+ // → projections for BOTH apply inline
8
+ //
9
+ // Without all three pieces wired together (registry opens defineEvent names,
10
+ // projections-runner fires on appendEvent, dispatcher routes appendEvent to
11
+ // the aggregate stream), any of the assertions below go red.
12
+
13
+ import { eq } from "drizzle-orm";
14
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
15
+ import { z } from "zod";
16
+ import {
17
+ integer as pgInteger,
18
+ table as pgTable,
19
+ text as pgText,
20
+ uuid as pgUuid,
21
+ } from "../../db/dialect";
22
+ import { createEventStoreExecutor } from "../../db/event-store-executor";
23
+ import { buildDrizzleTable } from "../../db/table-builder";
24
+ import { createEntity, createTextField, defineFeature } from "../../engine";
25
+ import { loadAggregate } from "../../event-store";
26
+ import {
27
+ createEntityTable,
28
+ resetEventStore,
29
+ setupTestStack,
30
+ type TestStack,
31
+ TestUsers,
32
+ } from "../../stack";
33
+
34
+ // --- Entity ---
35
+
36
+ const shipmentEntity = createEntity({
37
+ table: "read_domain_shipments",
38
+ fields: {
39
+ cargo: createTextField({ required: true }),
40
+ status: createTextField({ required: true }),
41
+ },
42
+ });
43
+
44
+ const shipmentTable = buildDrizzleTable("domain-shipment", shipmentEntity);
45
+
46
+ // --- Read-model table (fed by the projection below) ---
47
+
48
+ const billingTable = pgTable("read_domain_shipment_billing", {
49
+ shipmentId: pgUuid("shipment_id").primaryKey(),
50
+ tenantId: pgUuid("tenant_id").notNull(),
51
+ cargo: pgText("cargo").notNull(),
52
+ totalCost: pgInteger("total_cost").notNull().default(0),
53
+ billedMarker: pgText("billed_marker").notNull().default("pending"),
54
+ });
55
+
56
+ // --- Feature ---
57
+
58
+ const shippingFeature = defineFeature("shipping", (r) => {
59
+ r.entity("domain-shipment", shipmentEntity);
60
+
61
+ // Domain event. Qualified name is "shipping:event:billed".
62
+ const shipmentBilled = r.defineEvent("billed", z.object({ cost: z.number() }));
63
+
64
+ r.projection({
65
+ name: "shipment-billing",
66
+ source: "domain-shipment",
67
+ table: billingTable,
68
+ apply: {
69
+ // Auto CRUD event — fires on shipment create.
70
+ "domain-shipment.created": async (event, tx) => {
71
+ const payload = event.payload as { cargo?: string };
72
+ await tx.insert(billingTable).values({
73
+ shipmentId: event.aggregateId,
74
+ tenantId: event.tenantId,
75
+ cargo: payload.cargo ?? "",
76
+ totalCost: 0,
77
+ billedMarker: "pending",
78
+ });
79
+ },
80
+ // Domain event — fires on ctx.appendEvent. Same aggregate stream as
81
+ // the auto-event above, so the UPDATE below targets the row that the
82
+ // create-apply just inserted.
83
+ [shipmentBilled.name]: async (event, tx) => {
84
+ const payload = event.payload as { cost: number };
85
+ await tx
86
+ .update(billingTable)
87
+ .set({ totalCost: payload.cost, billedMarker: "billed" })
88
+ .where(eq(billingTable.shipmentId, event.aggregateId));
89
+ },
90
+ },
91
+ });
92
+
93
+ const shipmentExecutor = createEventStoreExecutor(shipmentTable, shipmentEntity, {
94
+ entityName: "domain-shipment",
95
+ });
96
+
97
+ r.writeHandler(
98
+ "shipment:create",
99
+ z.object({ cargo: z.string(), status: z.string() }),
100
+ async (event, ctx) => shipmentExecutor.create(event.payload, event.user, ctx.db),
101
+ { access: { roles: ["Admin"] } },
102
+ );
103
+
104
+ r.writeHandler(
105
+ "shipment:bill",
106
+ z.object({ id: z.uuid(), cost: z.number() }),
107
+ async (event, ctx) => {
108
+ await ctx.appendEventUnsafe({
109
+ aggregateId: event.payload.id,
110
+ aggregateType: "domain-shipment",
111
+ type: shipmentBilled.name,
112
+ payload: { cost: event.payload.cost },
113
+ });
114
+ return { isSuccess: true as const, data: { id: event.payload.id } };
115
+ },
116
+ { access: { roles: ["Admin"] } },
117
+ );
118
+
119
+ // CRUD update — used by the regression test to prove that a CRUD write
120
+ // *after* a ctx.appendEvent (which bumped the aggregate stream version)
121
+ // still finds the right expectedVersion.
122
+ r.writeHandler(
123
+ "shipment:update",
124
+ z.object({
125
+ id: z.uuid(),
126
+ version: z.number().optional(),
127
+ changes: z.record(z.string(), z.unknown()),
128
+ }),
129
+ async (event, ctx) => shipmentExecutor.update(event.payload, event.user, ctx.db),
130
+ { access: { roles: ["Admin"] } },
131
+ );
132
+
133
+ // Misuse probes — only exist so the tests below can exercise the rejection
134
+ // paths without spinning up a second feature (that would duplicate the
135
+ // entity and fail at registry build time).
136
+ r.writeHandler(
137
+ "shipment:bill-unregistered",
138
+ z.object({ id: z.uuid() }),
139
+ async (event, ctx) => {
140
+ await ctx.appendEventUnsafe({
141
+ aggregateId: event.payload.id,
142
+ aggregateType: "domain-shipment",
143
+ type: "shipping:event:ghost", // never defined via r.defineEvent
144
+ payload: {},
145
+ });
146
+ return { isSuccess: true as const, data: {} };
147
+ },
148
+ { access: { roles: ["Admin"] } },
149
+ );
150
+
151
+ r.writeHandler(
152
+ "shipment:bill-bad-payload",
153
+ z.object({ id: z.uuid() }),
154
+ async (event, ctx) => {
155
+ await ctx.appendEventUnsafe({
156
+ aggregateId: event.payload.id,
157
+ aggregateType: "domain-shipment",
158
+ type: shipmentBilled.name,
159
+ // cost must be a number per the defineEvent schema
160
+ payload: { cost: "definitely-not-a-number" } as unknown as { cost: number },
161
+ });
162
+ return { isSuccess: true as const, data: {} };
163
+ },
164
+ { access: { roles: ["Admin"] } },
165
+ );
166
+ });
167
+
168
+ // --- Test stack ---
169
+
170
+ let stack: TestStack;
171
+ const admin = TestUsers.admin;
172
+
173
+ beforeAll(async () => {
174
+ stack = await setupTestStack({
175
+ features: [shippingFeature],
176
+ systemHooks: [],
177
+ });
178
+ await createEntityTable(stack.db, shipmentEntity, "domain-shipment");
179
+ });
180
+
181
+ afterAll(async () => {
182
+ await stack.cleanup();
183
+ });
184
+
185
+ afterEach(async () => {
186
+ await resetEventStore(stack, ["read_domain_shipments", "read_domain_shipment_billing"]);
187
+ });
188
+
189
+ // --- Tests ---
190
+
191
+ describe("Marten gold-standard: domain events → inline projections", () => {
192
+ test("auto CRUD event triggers projection (regression guard)", async () => {
193
+ const data = await stack.http.writeOk<{ id: string }>(
194
+ "shipping:write:shipment:create",
195
+ { cargo: "Container A", status: "loaded" },
196
+ admin,
197
+ );
198
+
199
+ const rows = await stack.db.select().from(billingTable);
200
+ expect(rows).toHaveLength(1);
201
+ expect(rows[0]?.shipmentId).toBe(data.id);
202
+ expect(rows[0]?.billedMarker).toBe("pending");
203
+ expect(rows[0]?.totalCost).toBe(0);
204
+ });
205
+
206
+ test("ctx.appendEvent writes to the aggregate stream and triggers its projection", async () => {
207
+ const created = await stack.http.writeOk<{ id: string }>(
208
+ "shipping:write:shipment:create",
209
+ { cargo: "Container B", status: "loaded" },
210
+ admin,
211
+ );
212
+
213
+ await stack.http.writeOk("shipping:write:shipment:bill", { id: created.id, cost: 1500 }, admin);
214
+
215
+ const [row] = await stack.db
216
+ .select()
217
+ .from(billingTable)
218
+ .where(eq(billingTable.shipmentId, created.id));
219
+ expect(row).toBeDefined();
220
+ expect(row?.billedMarker).toBe("billed");
221
+ expect(row?.totalCost).toBe(1500);
222
+ });
223
+
224
+ test("domain event lives on the same aggregate stream as the auto event", async () => {
225
+ // The critical Marten invariant: every event belongs to one stream.
226
+ // The auto "created" event and the domain "billed" event must share
227
+ // (aggregateId, aggregateType) and be ordered by version.
228
+ const created = await stack.http.writeOk<{ id: string }>(
229
+ "shipping:write:shipment:create",
230
+ { cargo: "Container C", status: "loaded" },
231
+ admin,
232
+ );
233
+ await stack.http.writeOk("shipping:write:shipment:bill", { id: created.id, cost: 999 }, admin);
234
+
235
+ const events = await loadAggregate(stack.db, created.id, admin.tenantId);
236
+ expect(events).toHaveLength(2);
237
+ expect(events.map((e) => e.type)).toEqual(["domain-shipment.created", "shipping:event:billed"]);
238
+ expect(events.map((e) => e.version)).toEqual([1, 2]);
239
+ expect(events.every((e) => e.aggregateType === "domain-shipment")).toBe(true);
240
+ expect(events.every((e) => e.aggregateId === created.id)).toBe(true);
241
+ });
242
+
243
+ test("appendEvent with an unregistered type is rejected at the emit site", async () => {
244
+ // No r.defineEvent for "shipping:event:ghost" — the dispatcher must
245
+ // reject before the event reaches the events-table. Otherwise malformed
246
+ // events would only surface at consumer-time, durably persisted.
247
+ const created = await stack.http.writeOk<{ id: string }>(
248
+ "shipping:write:shipment:create",
249
+ { cargo: "Container D", status: "loaded" },
250
+ admin,
251
+ );
252
+
253
+ const res = await stack.http.write(
254
+ "shipping:write:shipment:bill-unregistered",
255
+ { id: created.id },
256
+ admin,
257
+ );
258
+ // Unknown event → InternalError → HTTP 500.
259
+ expect(res.status).toBe(500);
260
+
261
+ // Nothing for the ghost type is on disk.
262
+ const events = await loadAggregate(stack.db, created.id, admin.tenantId);
263
+ expect(events.some((e) => e.type === "shipping:event:ghost")).toBe(false);
264
+ });
265
+
266
+ test("Block 0 regression: CRUD update after ctx.appendEvent on same stream succeeds", async () => {
267
+ // Before Block 0, the CRUD executor read row.version as expectedVersion.
268
+ // ctx.appendEvent bumps the stream past that (billed = v2), and the next
269
+ // CRUD update would fail with events_aggregate_version_uq → version_conflict.
270
+ // After Block 0, getStreamVersion drives expectedVersion — both writers
271
+ // share the same stream cursor.
272
+ const created = await stack.http.writeOk<{ id: string; data: { version: number } }>(
273
+ "shipping:write:shipment:create",
274
+ { cargo: "Container Z", status: "loaded" },
275
+ admin,
276
+ );
277
+ // Create auto-event: stream at v1; projection row.version = 1
278
+ expect(created.data.version).toBe(1);
279
+
280
+ // ctx.appendEvent: stream at v2; projection row still at v1
281
+ await stack.http.writeOk("shipping:write:shipment:bill", { id: created.id, cost: 2500 }, admin);
282
+
283
+ // CRUD update — client presents the version it last saw (v2 is the
284
+ // current stream version). Without Block 0 this would fail: row.version
285
+ // is still 1, the local check rejects payload.version=2 as stale.
286
+ // With Block 0 the executor reads stream-version (2), accepts payload.version=2,
287
+ // and appends v3.
288
+ const updated = await stack.http.writeOk<{ data: { version: number } }>(
289
+ "shipping:write:shipment:update",
290
+ { id: created.id, version: 2, changes: { status: "delivered" } },
291
+ admin,
292
+ );
293
+ expect(updated.data.version).toBe(3);
294
+
295
+ const events = await loadAggregate(stack.db, created.id, admin.tenantId);
296
+ expect(events.map((e) => e.type)).toEqual([
297
+ "domain-shipment.created",
298
+ "shipping:event:billed",
299
+ "domain-shipment.updated",
300
+ ]);
301
+ expect(events.map((e) => e.version)).toEqual([1, 2, 3]);
302
+ });
303
+
304
+ test("payload validation runs before the event hits the events-table", async () => {
305
+ // defineEvent says cost is a number. Sending a string must abort the
306
+ // append (via Zod) — no malformed domain event ends up durable.
307
+ const created = await stack.http.writeOk<{ id: string }>(
308
+ "shipping:write:shipment:create",
309
+ { cargo: "Container E", status: "loaded" },
310
+ admin,
311
+ );
312
+
313
+ const res = await stack.http.write(
314
+ "shipping:write:shipment:bill-bad-payload",
315
+ { id: created.id },
316
+ admin,
317
+ );
318
+ expect([400, 422, 500]).toContain(res.status);
319
+
320
+ const events = await loadAggregate(stack.db, created.id, admin.tenantId);
321
+ expect(events.some((e) => e.type === "shipping:event:billed")).toBe(false);
322
+ });
323
+ });
@@ -0,0 +1,153 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
2
+ import { z } from "zod";
3
+ import { createEventStoreExecutor } from "../../db/event-store-executor";
4
+ import { defineFeature, type SaveContext } from "../../engine";
5
+ import {
6
+ createEntityTable,
7
+ createTestRedis,
8
+ setupTestStack,
9
+ type TestRedis,
10
+ type TestStack,
11
+ TestUsers,
12
+ } from "../../stack";
13
+ import { sharedItemEntity, sharedItemTable } from "../../testing";
14
+ import { createEventDedup } from "../event-dedup";
15
+
16
+ // --- Feature ---
17
+
18
+ const postSaveLog: SaveContext[] = [];
19
+
20
+ const dedupFeature = defineFeature("dedup", (r) => {
21
+ r.entity("item", sharedItemEntity);
22
+
23
+ r.writeHandler(
24
+ "item:create",
25
+ z.object({ name: z.string() }),
26
+ async (event, ctx) => {
27
+ const crud = createEventStoreExecutor(sharedItemTable, sharedItemEntity, {
28
+ entityName: "item",
29
+ });
30
+ return crud.create(event.payload, event.user, ctx.db);
31
+ },
32
+ { access: { roles: ["Admin"] } },
33
+ );
34
+
35
+ r.writeHandler(
36
+ "item:update",
37
+ z.object({
38
+ id: z.uuid(),
39
+ version: z.number().optional(),
40
+ changes: z.record(z.string(), z.unknown()),
41
+ }),
42
+ async (event, ctx) => {
43
+ const crud = createEventStoreExecutor(sharedItemTable, sharedItemEntity, {
44
+ entityName: "item",
45
+ });
46
+ return crud.update(event.payload, event.user, ctx.db);
47
+ },
48
+ { access: { roles: ["Admin"] } },
49
+ );
50
+
51
+ r.hook("postSave", "item:create", async (result) => {
52
+ postSaveLog.push(result);
53
+ });
54
+
55
+ r.hook("postSave", "item:update", async (result) => {
56
+ postSaveLog.push(result);
57
+ });
58
+ });
59
+
60
+ // --- Setup ---
61
+
62
+ let stack: TestStack;
63
+ let testRedis: TestRedis;
64
+ const admin = TestUsers.admin;
65
+
66
+ beforeAll(async () => {
67
+ testRedis = await createTestRedis();
68
+
69
+ stack = await setupTestStack({
70
+ features: [dedupFeature],
71
+ systemHooks: [],
72
+ });
73
+
74
+ await createEntityTable(stack.db, sharedItemEntity, "item");
75
+ });
76
+
77
+ afterAll(async () => {
78
+ await stack.cleanup();
79
+ await testRedis.cleanup();
80
+ });
81
+
82
+ beforeEach(() => {
83
+ postSaveLog.length = 0;
84
+ });
85
+
86
+ // =============================================================================
87
+ // Dedup at lifecycle level
88
+ // =============================================================================
89
+
90
+ describe("event dedup in lifecycle pipeline", () => {
91
+ test("postSave hooks fire normally (no dedup without eventDedup wired)", async () => {
92
+ await stack.http.writeOk("dedup:write:item:create", { name: "First" }, admin);
93
+
94
+ expect(postSaveLog).toHaveLength(1);
95
+ expect(postSaveLog[0]?.data["name"]).toBe("First");
96
+ });
97
+
98
+ test("two different creates both fire hooks", async () => {
99
+ await stack.http.writeOk("dedup:write:item:create", { name: "A" }, admin);
100
+ await stack.http.writeOk("dedup:write:item:create", { name: "B" }, admin);
101
+
102
+ expect(postSaveLog).toHaveLength(2);
103
+ });
104
+
105
+ test("two updates on same entity fire both hooks (different versions)", async () => {
106
+ const created = await stack.http.writeOk(
107
+ "dedup:write:item:create",
108
+ { name: "Versioned" },
109
+ admin,
110
+ );
111
+ postSaveLog.length = 0;
112
+
113
+ await stack.http.writeOk(
114
+ "dedup:write:item:update",
115
+ { id: created["id"], version: 1, changes: { name: "V2" } },
116
+ admin,
117
+ );
118
+ await stack.http.writeOk(
119
+ "dedup:write:item:update",
120
+ { id: created["id"], version: 2, changes: { name: "V3" } },
121
+ admin,
122
+ );
123
+
124
+ // Both updates should fire — version makes eventId unique
125
+ expect(postSaveLog).toHaveLength(2);
126
+ expect(postSaveLog[0]?.data["version"]).toBe(2);
127
+ expect(postSaveLog[1]?.data["version"]).toBe(3);
128
+ });
129
+ });
130
+
131
+ // =============================================================================
132
+ // Dedup guard direct test (lifecycle-level dedup behavior)
133
+ // =============================================================================
134
+
135
+ describe("event dedup guard blocks duplicate hook execution", () => {
136
+ test("tryAcquire prevents second execution for same eventId", async () => {
137
+ const dedup = createEventDedup(testRedis.redis, { ttlSeconds: 10 });
138
+
139
+ // Simulate: same handler + entity + version + phase
140
+ const eventId = "dedup.item.create:99:1:postSave";
141
+
142
+ expect(await dedup.tryAcquire(eventId)).toBe(true); // first: proceed
143
+ expect(await dedup.tryAcquire(eventId)).toBe(false); // duplicate: skip
144
+ });
145
+
146
+ test("different versions are independent", async () => {
147
+ const dedup = createEventDedup(testRedis.redis, { ttlSeconds: 10 });
148
+
149
+ expect(await dedup.tryAcquire("handler:5:1:postSave")).toBe(true);
150
+ expect(await dedup.tryAcquire("handler:5:2:postSave")).toBe(true); // different version
151
+ expect(await dedup.tryAcquire("handler:5:1:postSave")).toBe(false); // same as first
152
+ });
153
+ });
@@ -0,0 +1,202 @@
1
+ // E.3 — r.defineEvent + ctx.appendEvent strict-mode (B1).
2
+ //
3
+ // Pre-E.3 ctx accepted any string as eventType and any value as payload;
4
+ // payload went into the events-table raw, typos surfaced only at consumer-
5
+ // time. Now:
6
+ //
7
+ // 1. The event name MUST come from r.defineEvent. Unknown names throw
8
+ // InternalError with a clear hint at the append site.
9
+ // 2. The payload is validated against the registered Zod schema BEFORE
10
+ // it hits the events-table. Mismatches throw ValidationError.
11
+ // 3. r.defineEvent returns `{ name: qualifiedName, schema }` — callers
12
+ // pass `def.name` to ctx.appendEvent without building the qn manually.
13
+
14
+ import { eq } from "drizzle-orm";
15
+ import { afterEach, beforeAll, describe, expect, test } from "vitest";
16
+ import { z } from "zod";
17
+ import { defineFeature } from "../../engine";
18
+ import { eventsTable } from "../../event-store";
19
+ import {
20
+ createEntityTable,
21
+ resetEventStore,
22
+ setupTestStack,
23
+ type TestStack,
24
+ TestUsers,
25
+ } from "../../stack";
26
+ import { sharedWidgetEntity } from "../../testing";
27
+ import { generateId } from "../../utils";
28
+
29
+ // Capture of the qualified event name defineEvent returns so tests can
30
+ // assert against a moving target (kebab/qualifier transformations).
31
+ let welcomeEventName = "";
32
+ let foreignEventName = "";
33
+
34
+ // Second feature owning its own event. The emitter feature below tries to
35
+ // emit this from one of its handlers — Sprint-E cross-feature-ownership
36
+ // guard must reject it at the append site.
37
+ const neighborFeature = defineFeature("neighbor", (r) => {
38
+ const foreign = r.defineEvent("neighbor.signal", z.object({ userId: z.uuid() }));
39
+ foreignEventName = foreign.name;
40
+ });
41
+
42
+ const emitterFeature = defineFeature("emitter", (r) => {
43
+ r.entity("widget", sharedWidgetEntity);
44
+
45
+ const welcome = r.defineEvent("user.welcomed", z.object({ userId: z.uuid(), email: z.email() }));
46
+ welcomeEventName = welcome.name;
47
+
48
+ r.writeHandler(
49
+ "emit:valid",
50
+ z.object({ userId: z.uuid(), email: z.email() }),
51
+ async (cmd, ctx) => {
52
+ await ctx.appendEventUnsafe({
53
+ aggregateId: cmd.payload.userId,
54
+ aggregateType: "user",
55
+ type: welcome.name,
56
+ payload: cmd.payload,
57
+ });
58
+ return { isSuccess: true, data: { kind: "save", id: cmd.payload.userId } };
59
+ },
60
+ { access: { roles: ["Admin"] } },
61
+ );
62
+
63
+ r.writeHandler(
64
+ "emit:unknown-event-name",
65
+ z.object({ userId: z.uuid() }),
66
+ async (cmd, ctx) => {
67
+ // Deliberately NOT passing welcome.name — "emitter:event:not-registered"
68
+ // was never registered. ctx.appendEvent must reject at the append site.
69
+ await ctx.appendEventUnsafe({
70
+ aggregateId: cmd.payload.userId,
71
+ aggregateType: "user",
72
+ type: "emitter:event:not-registered",
73
+ payload: { userId: cmd.payload.userId },
74
+ });
75
+ return { isSuccess: true, data: { kind: "save", id: cmd.payload.userId } };
76
+ },
77
+ { access: { roles: ["Admin"] } },
78
+ );
79
+
80
+ r.writeHandler(
81
+ "emit:bad-payload",
82
+ z.object({ userId: z.uuid() }),
83
+ async (cmd, ctx) => {
84
+ // userId is correct but email is missing / not an email string.
85
+ await ctx.appendEventUnsafe({
86
+ aggregateId: cmd.payload.userId,
87
+ aggregateType: "user",
88
+ type: welcome.name,
89
+ payload: { userId: cmd.payload.userId, email: "not-an-email" },
90
+ });
91
+ return { isSuccess: true, data: { kind: "save", id: cmd.payload.userId } };
92
+ },
93
+ { access: { roles: ["Admin"] } },
94
+ );
95
+
96
+ r.writeHandler(
97
+ "emit:foreign-event",
98
+ z.object({ userId: z.uuid() }),
99
+ async (cmd, ctx) => {
100
+ // "neighbor:event:neighbor-signal" is owned by the neighbor feature.
101
+ // The ownership guard in appendDomainEventCore must reject this append
102
+ // at emit-site — cross-feature emission silently breaks encapsulation.
103
+ await ctx.appendEventUnsafe({
104
+ aggregateId: cmd.payload.userId,
105
+ aggregateType: "user",
106
+ type: foreignEventName,
107
+ payload: { userId: cmd.payload.userId },
108
+ });
109
+ return { isSuccess: true, data: { kind: "save", id: cmd.payload.userId } };
110
+ },
111
+ { access: { roles: ["Admin"] } },
112
+ );
113
+ });
114
+
115
+ const admin = TestUsers.admin;
116
+ let stack: TestStack;
117
+
118
+ beforeAll(async () => {
119
+ stack = await setupTestStack({
120
+ features: [emitterFeature, neighborFeature],
121
+ systemHooks: [],
122
+ });
123
+ await createEntityTable(stack.db, sharedWidgetEntity, "widget");
124
+ });
125
+
126
+ afterEach(async () => {
127
+ await resetEventStore(stack, ["read_widgets"]);
128
+ });
129
+
130
+ // --- Tests ---
131
+
132
+ describe("E.3 — r.defineEvent return + registry wiring", () => {
133
+ test("defineEvent returns qualified name matching registry lookup", () => {
134
+ expect(welcomeEventName).toBe("emitter:event:user-welcomed");
135
+ expect(stack.registry.getEvent(welcomeEventName)).toBeDefined();
136
+ });
137
+ });
138
+
139
+ describe("E.3 — ctx.appendEvent strict validation", () => {
140
+ test("valid append lands in the events-table with the qualified type on the aggregate stream", async () => {
141
+ const userId = generateId();
142
+ const res = await stack.http.write(
143
+ "emitter:write:emit:valid",
144
+ { userId, email: "welcome@test.de" },
145
+ admin,
146
+ );
147
+ expect(res.status).toBe(200);
148
+
149
+ const stored = await stack.db
150
+ .select()
151
+ .from(eventsTable)
152
+ .where(eq(eventsTable.type, welcomeEventName));
153
+ expect(stored).toHaveLength(1);
154
+ expect(stored[0]?.payload).toEqual({ userId, email: "welcome@test.de" });
155
+ expect(stored[0]?.aggregateType).toBe("user");
156
+ expect(stored[0]?.aggregateId).toBe(userId);
157
+ // Fresh aggregate → version 1 (Block 0 getStreamVersion returns 0, append bumps to 1).
158
+ expect(stored[0]?.version).toBe(1);
159
+ });
160
+
161
+ test("unknown event name throws InternalError; nothing lands in the log", async () => {
162
+ const userId = generateId();
163
+ const res = await stack.http.write("emitter:write:emit:unknown-event-name", { userId }, admin);
164
+ // Non-Kumiko throw inside a handler → wrapped as InternalError → 500.
165
+ expect(res.status).toBe(500);
166
+
167
+ const stored = await stack.db.select().from(eventsTable);
168
+ // TX rolled back: no event landed.
169
+ expect(stored).toHaveLength(0);
170
+ });
171
+
172
+ test("payload mismatch throws ValidationError; event not persisted", async () => {
173
+ const userId = generateId();
174
+ const res = await stack.http.write("emitter:write:emit:bad-payload", { userId }, admin);
175
+ // ValidationError is a first-class Kumiko error → 400.
176
+ expect(res.status).toBe(400);
177
+
178
+ const stored = await stack.db
179
+ .select()
180
+ .from(eventsTable)
181
+ .where(eq(eventsTable.type, welcomeEventName));
182
+ expect(stored).toHaveLength(0);
183
+ });
184
+ });
185
+
186
+ describe("E.3 — cross-feature ownership guard", () => {
187
+ test("emitter cannot ctx.appendEvent an event owned by another feature", async () => {
188
+ // neighbor:event:neighbor-signal is a registered event — but it lives
189
+ // in the neighbor feature, not the emitter. Without the guard the
190
+ // append would succeed silently, attaching a "foreign" event onto the
191
+ // emitter's aggregate stream and undermining feature encapsulation.
192
+ const userId = generateId();
193
+ const res = await stack.http.write("emitter:write:emit:foreign-event", { userId }, admin);
194
+ expect(res.status).toBe(500);
195
+
196
+ const stored = await stack.db
197
+ .select()
198
+ .from(eventsTable)
199
+ .where(eq(eventsTable.type, foreignEventName));
200
+ expect(stored).toHaveLength(0);
201
+ });
202
+ });