@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,395 @@
1
+ // Schema-Drift-Detection für den Boot-Gate und die migrate-validate-CLI.
2
+ //
3
+ // Vergleicht den Drizzle-Migrations-Stand (committed im Repo unter
4
+ // drizzle/migrations/meta/) mit dem aktuellen DB-Stand. Drei Schichten:
5
+ //
6
+ // 1. Journal-vs-Applied: jeder Eintrag im _journal.json muss eine Zeile
7
+ // in __drizzle_migrations haben (= migrate apply lief vollständig).
8
+ // 2. Tables-Exist: jede Tabelle aus dem letzten Snapshot existiert.
9
+ // 3. Column-Diff: information_schema-Vergleich gegen Snapshot —
10
+ // missing-/extra-column, type-mismatch, nullability-mismatch. Fängt
11
+ // manuelle ALTER TABLEs in Prod sowie doppelte pgTable-Definitionen
12
+ // pro Tabelle (eine hand-written, eine via buildDrizzleTable), die
13
+ // stillschweigend gegen den Snapshot driften.
14
+ //
15
+ // Drizzle-kit's eigene Garantie: nach `migrate apply` ist der DB-Stand
16
+ // strukturell identisch mit dem letzten Snapshot. Schicht 3 catched
17
+ // alles was diese Garantie nachträglich bricht — schreibender Drittsystem,
18
+ // veraltete Code-Definitionen, vergessenes generate.
19
+
20
+ import { readFileSync } from "node:fs";
21
+ import { resolve } from "node:path";
22
+ import { sql } from "drizzle-orm";
23
+ import type { DbConnection } from "../db/connection";
24
+ import { tableExists } from "../db/schema-inspection";
25
+ import { parseJsonOrThrow } from "../utils/safe-json";
26
+
27
+ // --- Journal & Snapshot Loader ---
28
+
29
+ export type JournalEntry = {
30
+ readonly idx: number;
31
+ readonly version: string;
32
+ readonly when: number;
33
+ readonly tag: string;
34
+ readonly breakpoints: boolean;
35
+ };
36
+
37
+ export type Journal = {
38
+ readonly version: string;
39
+ readonly dialect: string;
40
+ readonly entries: readonly JournalEntry[];
41
+ };
42
+
43
+ export function loadJournal(migrationsDir: string): Journal {
44
+ const journalPath = resolve(migrationsDir, "meta/_journal.json");
45
+ return parseJsonOrThrow<Journal>(readFileSync(journalPath, "utf-8"), `journal at ${journalPath}`);
46
+ }
47
+
48
+ /** Drizzle-Snapshot-Format. Eine Type für alle Read-Pfade — der
49
+ * Boot-Gate liest nur table-name+schema, projection-detection liest
50
+ * zusätzlich columns. Optional-typed `columns`-Field hält den Loader
51
+ * monomorph ohne zwei verschiedene Snapshot-Types. */
52
+ export type ColumnSpec = {
53
+ readonly name: string;
54
+ readonly type: string;
55
+ readonly notNull?: boolean;
56
+ readonly primaryKey?: boolean;
57
+ readonly default?: unknown;
58
+ };
59
+
60
+ export type SnapshotTable = {
61
+ readonly schema: string;
62
+ readonly name: string;
63
+ readonly columns: Readonly<Record<string, ColumnSpec>>;
64
+ };
65
+
66
+ export type Snapshot = {
67
+ readonly tables: Readonly<Record<string, SnapshotTable>>;
68
+ };
69
+
70
+ export function loadSnapshot(snapshotPath: string): Snapshot {
71
+ return parseJsonOrThrow<Snapshot>(
72
+ readFileSync(snapshotPath, "utf-8"),
73
+ `snapshot at ${snapshotPath}`,
74
+ );
75
+ }
76
+
77
+ function snapshotPathForIdx(migrationsDir: string, idx: number): string {
78
+ return resolve(migrationsDir, "meta", `${String(idx).padStart(4, "0")}_snapshot.json`);
79
+ }
80
+
81
+ /** Letzter Snapshot — der Stand der durch das jüngste Migration-File
82
+ * beschrieben ist. Wirft wenn das Journal leer ist (App ohne erste
83
+ * Migration). */
84
+ export function loadLatestSnapshot(migrationsDir: string): Snapshot {
85
+ const journal = loadJournal(migrationsDir);
86
+ const latest = journal.entries[journal.entries.length - 1];
87
+ if (!latest) {
88
+ throw new Error(
89
+ `loadLatestSnapshot: no entries in ${resolve(migrationsDir, "meta/_journal.json")}. ` +
90
+ `Run 'yarn kumiko migrate generate' first.`,
91
+ );
92
+ }
93
+ return loadSnapshot(snapshotPathForIdx(migrationsDir, latest.idx));
94
+ }
95
+
96
+ /** Vorletzter Snapshot — für Diff-Operationen. Returns null wenn
97
+ * weniger als 2 Einträge im Journal (Initial-Migration kann gegen
98
+ * nichts diff'en). */
99
+ export function loadPreviousSnapshot(migrationsDir: string): Snapshot | null {
100
+ const journal = loadJournal(migrationsDir);
101
+ if (journal.entries.length < 2) return null;
102
+ const previous = journal.entries[journal.entries.length - 2];
103
+ if (!previous) return null;
104
+ return loadSnapshot(snapshotPathForIdx(migrationsDir, previous.idx));
105
+ }
106
+
107
+ // --- DB-State Inspector ---
108
+
109
+ export type AppliedMigration = {
110
+ readonly hash: string;
111
+ readonly createdAt: number;
112
+ };
113
+
114
+ /** Liest die `__drizzle_migrations`-Tabelle. Wenn sie nicht existiert
115
+ * (frische DB, niemand hat bisher migrate apply gefahren) → leeres
116
+ * Array. Caller soll daraus "alle pending"-Drift ableiten.
117
+ *
118
+ * Drizzle-kit aktuell speichert in `drizzle.__drizzle_migrations`
119
+ * (eigenes Schema), Pre-0.20-Versionen in `public.__drizzle_migrations`.
120
+ * Wir prüfen beide Pfade und queryen den vorhandenen — keine
121
+ * hardcoded Schema-Annahme. */
122
+ export async function loadAppliedMigrations(db: DbConnection): Promise<AppliedMigration[]> {
123
+ const drizzleSchemaExists = await tableExists(db, "drizzle.__drizzle_migrations");
124
+ const publicSchemaExists = drizzleSchemaExists
125
+ ? false
126
+ : await tableExists(db, "public.__drizzle_migrations");
127
+ if (!drizzleSchemaExists && !publicSchemaExists) return [];
128
+ // sql.identifier mit qualifiziertem Namen: erstes Argument = Schema,
129
+ // zweites = Tabellenname. Drizzle quotet beides defensiv.
130
+ const tableRef = drizzleSchemaExists
131
+ ? sql`drizzle.__drizzle_migrations`
132
+ : sql`public.__drizzle_migrations`;
133
+ const rows = await db.execute<{ hash: string; created_at: bigint | number | null }>(sql`
134
+ SELECT hash, created_at
135
+ FROM ${tableRef}
136
+ ORDER BY id
137
+ `);
138
+ return rows.map((r) => ({
139
+ hash: r.hash,
140
+ createdAt: typeof r.created_at === "bigint" ? Number(r.created_at) : (r.created_at ?? 0),
141
+ }));
142
+ }
143
+
144
+ // --- Column-Diff (Welle 2 Boot-Gate Layer 3) ---
145
+
146
+ type DbColumnRow = {
147
+ readonly column_name: string;
148
+ readonly data_type: string;
149
+ readonly is_nullable: "YES" | "NO";
150
+ };
151
+
152
+ /** Liest information_schema.columns für eine Tabelle im public-Schema.
153
+ * Map by column_name. Default-Werte werden bewusst ausgelassen — die
154
+ * drift'en über drizzle-Versionen / PG-Reformulierungen hinweg ohne dass
155
+ * sich faktisch was ändert (z.B. `now()` vs `CURRENT_TIMESTAMP`). Type +
156
+ * notNull sind die belastbaren Vergleichs-Felder. */
157
+ async function loadDbColumns(
158
+ db: DbConnection,
159
+ tableName: string,
160
+ ): Promise<ReadonlyMap<string, { type: string; notNull: boolean }>> {
161
+ const rows = await db.execute<DbColumnRow>(sql`
162
+ SELECT column_name, data_type, is_nullable
163
+ FROM information_schema.columns
164
+ WHERE table_schema = 'public' AND table_name = ${tableName}
165
+ `);
166
+ const map = new Map<string, { type: string; notNull: boolean }>();
167
+ for (const r of rows) {
168
+ map.set(r.column_name, {
169
+ type: normalizePgType(r.data_type),
170
+ notNull: r.is_nullable === "NO",
171
+ });
172
+ }
173
+ return map;
174
+ }
175
+
176
+ /** Normalize PG type-Strings auf Drizzle-Snapshot-Konvention. PG meldet
177
+ * "timestamp with time zone" für TIMESTAMPTZ, "character varying" für
178
+ * VARCHAR — Drizzle schreibt "timestamp with time zone" / "varchar" im
179
+ * Snapshot. Wir kollabieren auf einen kanonischen String. */
180
+ function normalizePgType(pgType: string): string {
181
+ switch (pgType) {
182
+ case "timestamp with time zone":
183
+ return "timestamp with time zone";
184
+ case "character varying":
185
+ return "varchar";
186
+ case "double precision":
187
+ return "double precision";
188
+ case "USER-DEFINED":
189
+ // Custom-types wie enums — kein clean diff möglich, akzeptieren wir
190
+ // als "irgendwas" und überspringen die Type-Prüfung.
191
+ return "USER-DEFINED";
192
+ default:
193
+ return pgType;
194
+ }
195
+ }
196
+
197
+ function normalizeSnapshotType(snapshotType: string): string {
198
+ // PostgreSQL meldet im information_schema kanonisierte data_type-Strings,
199
+ // Drizzle's snapshot kann mehrere äquivalente Schreibweisen produzieren:
200
+ //
201
+ // timestamptz → "timestamp with time zone"
202
+ // timestamp(3) with time zone → "timestamp with time zone"
203
+ // timestamp without time zone → unverändert
204
+ // bigserial → "bigint" (serial ist Macro für sequence + bigint)
205
+ // serial → "integer"
206
+ // smallserial → "smallint"
207
+ // varchar(N) → "character varying"
208
+ //
209
+ // Ohne diese Normalisierung produziert Layer-3 false-positives weil DB
210
+ // und Snapshot semantisch dieselbe Spalte unterschiedlich schreiben.
211
+ const lower = snapshotType.toLowerCase().replace(/\s+/g, " ").trim();
212
+ if (lower === "timestamptz" || lower.match(/^timestamp\(\d+\) with time zone$/)) {
213
+ return "timestamp with time zone";
214
+ }
215
+ if (lower === "bigserial") return "bigint";
216
+ if (lower === "serial") return "integer";
217
+ if (lower === "smallserial") return "smallint";
218
+ if (lower.startsWith("varchar")) return "character varying";
219
+ return lower;
220
+ }
221
+
222
+ /** Eine Differenz zwischen erwarteter (Snapshot) und tatsächlicher (DB)
223
+ * Spalten-Definition. */
224
+ export type ColumnIssue =
225
+ | { readonly kind: "missing-column"; readonly table: string; readonly column: string }
226
+ | { readonly kind: "extra-column"; readonly table: string; readonly column: string }
227
+ | {
228
+ readonly kind: "type-mismatch";
229
+ readonly table: string;
230
+ readonly column: string;
231
+ readonly expected: string;
232
+ readonly actual: string;
233
+ }
234
+ | {
235
+ readonly kind: "nullability-mismatch";
236
+ readonly table: string;
237
+ readonly column: string;
238
+ readonly expectedNotNull: boolean;
239
+ readonly actualNotNull: boolean;
240
+ };
241
+
242
+ async function detectColumnIssues(
243
+ db: DbConnection,
244
+ snapshot: Snapshot,
245
+ existingTables: readonly string[],
246
+ ): Promise<readonly ColumnIssue[]> {
247
+ const issues: ColumnIssue[] = [];
248
+ const existingSet = new Set(existingTables);
249
+ for (const t of Object.values(snapshot.tables)) {
250
+ const fullName = t.schema && t.schema.length > 0 ? `${t.schema}.${t.name}` : t.name;
251
+ if (!existingSet.has(fullName)) continue; // missing-table-Layer hat das schon
252
+ const dbCols = await loadDbColumns(db, t.name);
253
+ const snapCols = t.columns;
254
+ // Spalten die im Snapshot stehen, aber nicht in der DB sind.
255
+ for (const snapCol of Object.values(snapCols)) {
256
+ const dbCol = dbCols.get(snapCol.name);
257
+ if (!dbCol) {
258
+ issues.push({ kind: "missing-column", table: t.name, column: snapCol.name });
259
+ continue;
260
+ }
261
+ const expectedType = normalizeSnapshotType(snapCol.type);
262
+ // USER-DEFINED ist die PG-Antwort für enums — type-Vergleich wäre
263
+ // unzuverlässig (PG meldet keinen Enum-Namen über data_type). Skip.
264
+ if (dbCol.type !== "USER-DEFINED" && dbCol.type !== expectedType) {
265
+ issues.push({
266
+ kind: "type-mismatch",
267
+ table: t.name,
268
+ column: snapCol.name,
269
+ expected: expectedType,
270
+ actual: dbCol.type,
271
+ });
272
+ }
273
+ const expectedNotNull = snapCol.notNull === true || snapCol.primaryKey === true;
274
+ if (dbCol.notNull !== expectedNotNull) {
275
+ issues.push({
276
+ kind: "nullability-mismatch",
277
+ table: t.name,
278
+ column: snapCol.name,
279
+ expectedNotNull,
280
+ actualNotNull: dbCol.notNull,
281
+ });
282
+ }
283
+ }
284
+ // Spalten die in der DB sind, aber nicht im Snapshot — vermutlich
285
+ // manueller ALTER TABLE in Prod. Reportet als extra-column.
286
+ const snapDbNames = new Set(Object.values(snapCols).map((c) => c.name));
287
+ for (const dbColName of dbCols.keys()) {
288
+ if (!snapDbNames.has(dbColName)) {
289
+ issues.push({ kind: "extra-column", table: t.name, column: dbColName });
290
+ }
291
+ }
292
+ }
293
+ return issues;
294
+ }
295
+
296
+ // --- Drift Report ---
297
+
298
+ export type DriftReport = {
299
+ readonly ok: boolean;
300
+ readonly pendingMigrations: readonly JournalEntry[];
301
+ readonly missingTables: readonly string[];
302
+ readonly columnIssues: readonly ColumnIssue[];
303
+ };
304
+
305
+ export async function detectDrift(db: DbConnection, migrationsDir: string): Promise<DriftReport> {
306
+ const journal = loadJournal(migrationsDir);
307
+ const applied = await loadAppliedMigrations(db);
308
+
309
+ // Heuristik: Drizzle's `__drizzle_migrations` enthält keine Reihenfolge-
310
+ // Information die direkt zu journal.tag matched. Praktisch: nach jeder
311
+ // erfolgreichen `migrate apply` ist applied.length === entries.length.
312
+ // Wenn Count abweicht → pending.
313
+ const pendingMigrations =
314
+ applied.length < journal.entries.length ? journal.entries.slice(applied.length) : [];
315
+
316
+ const snapshot = loadLatestSnapshot(migrationsDir);
317
+ // Drizzle's snapshot schreibt `schema: ""` für public — to_regclass
318
+ // ohne Schema-Prefix resolved ebenfalls in public, also passt empty.
319
+ const expectedTables = Object.values(snapshot.tables).map((t) =>
320
+ t.schema && t.schema.length > 0 ? `${t.schema}.${t.name}` : t.name,
321
+ );
322
+ const exists = await Promise.all(expectedTables.map((q) => tableExists(db, q)));
323
+ const missingTables = expectedTables.filter((_, i) => !exists[i]);
324
+ const existingTables = expectedTables.filter((_, i) => exists[i]);
325
+
326
+ // Layer 3: Column-Diff für die Tables die existieren. Pending Migrations
327
+ // skippen wir — die DB ist ohnehin in einem Zwischenzustand.
328
+ const columnIssues =
329
+ pendingMigrations.length === 0 ? await detectColumnIssues(db, snapshot, existingTables) : [];
330
+
331
+ return {
332
+ ok: pendingMigrations.length === 0 && missingTables.length === 0 && columnIssues.length === 0,
333
+ pendingMigrations,
334
+ missingTables,
335
+ columnIssues,
336
+ };
337
+ }
338
+
339
+ export function formatDriftReport(report: DriftReport): string {
340
+ if (report.ok) return "Schema is current.";
341
+ const lines: string[] = ["Schema drift detected:"];
342
+ if (report.pendingMigrations.length > 0) {
343
+ lines.push(` ${report.pendingMigrations.length} unapplied migration(s):`);
344
+ for (const m of report.pendingMigrations) {
345
+ lines.push(` - ${m.tag}`);
346
+ }
347
+ }
348
+ if (report.missingTables.length > 0) {
349
+ lines.push(` ${report.missingTables.length} missing table(s):`);
350
+ for (const t of report.missingTables) {
351
+ lines.push(` - ${t}`);
352
+ }
353
+ }
354
+ if (report.columnIssues.length > 0) {
355
+ lines.push(` ${report.columnIssues.length} column issue(s):`);
356
+ for (const issue of report.columnIssues) {
357
+ switch (issue.kind) {
358
+ case "missing-column":
359
+ lines.push(` - ${issue.table}.${issue.column}: missing in DB`);
360
+ break;
361
+ case "extra-column":
362
+ lines.push(` - ${issue.table}.${issue.column}: not in snapshot`);
363
+ break;
364
+ case "type-mismatch":
365
+ lines.push(
366
+ ` - ${issue.table}.${issue.column}: type ${issue.actual} (expected ${issue.expected})`,
367
+ );
368
+ break;
369
+ case "nullability-mismatch":
370
+ lines.push(
371
+ ` - ${issue.table}.${issue.column}: nullable=${!issue.actualNotNull} (expected nullable=${!issue.expectedNotNull})`,
372
+ );
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ lines.push("");
378
+ lines.push("Run 'yarn kumiko migrate apply' to bring the DB up-to-date.");
379
+ return lines.join("\n");
380
+ }
381
+
382
+ /** Throws SchemaDriftError mit human-readable message wenn Drift. */
383
+ export async function assertSchemaCurrent(db: DbConnection, migrationsDir: string): Promise<void> {
384
+ const report = await detectDrift(db, migrationsDir);
385
+ if (!report.ok) throw new SchemaDriftError(formatDriftReport(report), report);
386
+ }
387
+
388
+ export class SchemaDriftError extends Error {
389
+ readonly report: DriftReport;
390
+ constructor(message: string, report: DriftReport) {
391
+ super(message);
392
+ this.name = "SchemaDriftError";
393
+ this.report = report;
394
+ }
395
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createConsoleProvider } from "../console-provider";
3
+
4
+ function makeProvider() {
5
+ const lines: string[] = [];
6
+ const provider = createConsoleProvider({
7
+ writer: { log: (l) => lines.push(l) },
8
+ });
9
+ return { provider, lines };
10
+ }
11
+
12
+ describe("ConsoleProvider", () => {
13
+ it("prints the full span tree once root ends", async () => {
14
+ const { provider, lines } = makeProvider();
15
+ await provider.tracer.withSpan("http.request", async () => {
16
+ await provider.tracer.withSpan("db.query", async (span) => {
17
+ span.setAttribute("db.table", "orders");
18
+ });
19
+ await provider.tracer.withSpan("redis.cmd", async () => {});
20
+ });
21
+ expect(lines).toHaveLength(1);
22
+ const output = lines[0]!;
23
+ expect(output).toContain("http.request");
24
+ expect(output).toContain("db.query");
25
+ expect(output).toContain("redis.cmd");
26
+ expect(output).toContain("db.table=orders");
27
+ });
28
+
29
+ it("marks errored spans with [ERR]", async () => {
30
+ const { provider, lines } = makeProvider();
31
+ await expect(
32
+ provider.tracer.withSpan("http.request", async () => {
33
+ throw new Error("boom");
34
+ }),
35
+ ).rejects.toThrow("boom");
36
+ expect(lines[0]).toContain("[ERR]");
37
+ expect(lines[0]).toContain("!exception=Error: boom");
38
+ });
39
+
40
+ it("emits metric events as log lines", () => {
41
+ const { provider, lines } = makeProvider();
42
+ provider.meter.registerMetric({
43
+ name: "kumiko_orders_created_total",
44
+ type: "counter",
45
+ });
46
+ provider.meter.counter("kumiko_orders_created_total").inc();
47
+ expect(lines[0]).toContain("counter.inc");
48
+ expect(lines[0]).toContain("kumiko_orders_created_total");
49
+ expect(lines[0]).toContain("value=1");
50
+ });
51
+
52
+ it("renders nested tree with correct hierarchy", async () => {
53
+ const { provider, lines } = makeProvider();
54
+ await provider.tracer.withSpan("http.request", async () => {
55
+ await provider.tracer.withSpan("kumiko.dispatcher.handler", async () => {
56
+ await provider.tracer.withSpan("db.query", async () => {});
57
+ });
58
+ });
59
+ const out = lines[0]!;
60
+ const httpIdx = out.indexOf("http.request");
61
+ const dispatcherIdx = out.indexOf("kumiko.dispatcher.handler");
62
+ const dbIdx = out.indexOf("db.query");
63
+ expect(httpIdx).toBeGreaterThanOrEqual(0);
64
+ expect(dispatcherIdx).toBeGreaterThan(httpIdx);
65
+ expect(dbIdx).toBeGreaterThan(dispatcherIdx);
66
+ });
67
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildMetricName, validateLabelKey, validateMetricName } from "../metric-validator";
3
+
4
+ describe("validateMetricName", () => {
5
+ describe("counter", () => {
6
+ it("accepts _total suffix", () => {
7
+ expect(() => validateMetricName("orders_created_total", "counter")).not.toThrow();
8
+ });
9
+
10
+ it("rejects missing _total suffix", () => {
11
+ expect(() => validateMetricName("orders_created", "counter")).toThrow(
12
+ /must end with "_total"/,
13
+ );
14
+ });
15
+
16
+ it("rejects camelCase", () => {
17
+ expect(() => validateMetricName("ordersCreatedTotal", "counter")).toThrow(/snake_case/);
18
+ });
19
+
20
+ it("rejects leading digit", () => {
21
+ expect(() => validateMetricName("1_orders_total", "counter")).toThrow(/snake_case/);
22
+ });
23
+ });
24
+
25
+ describe("histogram", () => {
26
+ it("accepts _seconds suffix", () => {
27
+ expect(() => validateMetricName("http_request_duration_seconds", "histogram")).not.toThrow();
28
+ });
29
+
30
+ it("accepts _bytes suffix", () => {
31
+ expect(() => validateMetricName("http_request_body_bytes", "histogram")).not.toThrow();
32
+ });
33
+
34
+ it("accepts custom domain unit (_eur)", () => {
35
+ expect(() => validateMetricName("orders_value_eur", "histogram")).not.toThrow();
36
+ });
37
+
38
+ it("rejects _total suffix", () => {
39
+ expect(() => validateMetricName("http_request_total", "histogram")).toThrow(
40
+ /must not end with "_total"/,
41
+ );
42
+ });
43
+
44
+ it("rejects single word without unit", () => {
45
+ expect(() => validateMetricName("duration", "histogram")).toThrow(/needs a unit suffix/);
46
+ });
47
+ });
48
+
49
+ describe("gauge", () => {
50
+ it("accepts plain noun", () => {
51
+ expect(() => validateMetricName("db_pool_active_connections", "gauge")).not.toThrow();
52
+ });
53
+
54
+ it("rejects _total suffix", () => {
55
+ expect(() => validateMetricName("active_sessions_total", "gauge")).toThrow(/_total/);
56
+ });
57
+
58
+ it("rejects _seconds suffix (suggests histogram)", () => {
59
+ expect(() => validateMetricName("request_duration_seconds", "gauge")).toThrow(/histogram/i);
60
+ });
61
+ });
62
+ });
63
+
64
+ describe("buildMetricName", () => {
65
+ it("prefixes with kumiko_<feature>_", () => {
66
+ expect(buildMetricName("orders", "created_total")).toBe("kumiko_orders_created_total");
67
+ });
68
+
69
+ it("rejects non-snake_case feature name", () => {
70
+ expect(() => buildMetricName("Orders", "created_total")).toThrow(/snake_case/);
71
+ });
72
+ });
73
+
74
+ describe("validateLabelKey", () => {
75
+ it("accepts snake_case", () => {
76
+ expect(() => validateLabelKey("error_class")).not.toThrow();
77
+ });
78
+
79
+ it("rejects camelCase", () => {
80
+ expect(() => validateLabelKey("errorClass")).toThrow(/snake_case/);
81
+ });
82
+
83
+ it("rejects reserved Prometheus keys", () => {
84
+ expect(() => validateLabelKey("le")).toThrow(/reserved/);
85
+ expect(() => validateLabelKey("__name__")).toThrow(/snake_case/);
86
+ });
87
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createNoopProvider } from "../noop-provider";
3
+
4
+ describe("NoopProvider", () => {
5
+ it("provides noop tracer with startSpan", () => {
6
+ const p = createNoopProvider();
7
+ const span = p.tracer.startSpan("test");
8
+ expect(span.name).toBe("test");
9
+ expect(span.traceId).toBe("");
10
+ expect(span.ended).toBe(false);
11
+ span.setAttribute("foo", "bar");
12
+ span.end();
13
+ expect(span.ended).toBe(true);
14
+ });
15
+
16
+ it("withSpan runs fn and returns its value", async () => {
17
+ const p = createNoopProvider();
18
+ const result = await p.tracer.withSpan("op", async (span) => {
19
+ expect(span.name).toBe("op");
20
+ return 42;
21
+ });
22
+ expect(result).toBe(42);
23
+ });
24
+
25
+ it("withSpan ends span on thrown error", async () => {
26
+ const p = createNoopProvider();
27
+ let capturedSpan: { ended: boolean } | undefined;
28
+ await expect(
29
+ p.tracer.withSpan("boom", async (span) => {
30
+ capturedSpan = span;
31
+ throw new Error("boom");
32
+ }),
33
+ ).rejects.toThrow("boom");
34
+ expect(capturedSpan?.ended).toBe(true);
35
+ });
36
+
37
+ it("getActiveSpan returns undefined (noop doesn't propagate)", () => {
38
+ const p = createNoopProvider();
39
+ expect(p.tracer.getActiveSpan()).toBeUndefined();
40
+ });
41
+
42
+ it("registerMetric rejects duplicates", () => {
43
+ const p = createNoopProvider();
44
+ p.meter.registerMetric({
45
+ name: "kumiko_test_total",
46
+ type: "counter",
47
+ });
48
+ expect(() =>
49
+ p.meter.registerMetric({
50
+ name: "kumiko_test_total",
51
+ type: "counter",
52
+ }),
53
+ ).toThrow(/already registered/);
54
+ });
55
+
56
+ it("meter returns typed handles for registered metrics", () => {
57
+ const p = createNoopProvider();
58
+ p.meter.registerMetric({ name: "kumiko_test_total", type: "counter" });
59
+ p.meter.registerMetric({ name: "kumiko_test_duration_seconds", type: "histogram" });
60
+ p.meter.registerMetric({ name: "kumiko_test_pool", type: "gauge" });
61
+
62
+ expect(() => p.meter.counter("kumiko_test_total").inc()).not.toThrow();
63
+ expect(() => p.meter.histogram("kumiko_test_duration_seconds").observe(0.5)).not.toThrow();
64
+ expect(() => p.meter.gauge("kumiko_test_pool").set(10)).not.toThrow();
65
+ });
66
+
67
+ it("meter rejects wrong type lookup", () => {
68
+ const p = createNoopProvider();
69
+ p.meter.registerMetric({ name: "kumiko_test_total", type: "counter" });
70
+ expect(() => p.meter.histogram("kumiko_test_total")).toThrow(/not registered or wrong type/);
71
+ });
72
+
73
+ it("meter rejects unknown metric", () => {
74
+ const p = createNoopProvider();
75
+ expect(() => p.meter.counter("kumiko_nothing_total")).toThrow(/not registered/);
76
+ });
77
+
78
+ it("shutdown resolves", async () => {
79
+ const p = createNoopProvider();
80
+ await expect(p.shutdown()).resolves.toBeUndefined();
81
+ });
82
+ });