@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.
- package/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared Set-Cookie parser for tests. Cookie-auth tests (auth-middleware,
|
|
2
|
+
// csrf-middleware, auth-routes, auth.integration) all need the same
|
|
3
|
+
// primitive: "given a Response, give me the cookies the server tried to
|
|
4
|
+
// set". Before this helper each test file rolled its own — three near-
|
|
5
|
+
// duplicates that drift the moment one adds Domain/Partitioned/Priority
|
|
6
|
+
// parsing the others don't.
|
|
7
|
+
//
|
|
8
|
+
// The helper returns both the parsed value AND the raw string so callers
|
|
9
|
+
// can assert on attributes like SameSite, HttpOnly, Max-Age without a
|
|
10
|
+
// second parse step.
|
|
11
|
+
|
|
12
|
+
export type ParsedSetCookie = {
|
|
13
|
+
readonly value: string;
|
|
14
|
+
readonly raw: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Pull every Set-Cookie the response carries. Prefers the standard
|
|
18
|
+
// `Headers.getSetCookie()` (Node 20+, Bun, modern undici/whatwg-fetch);
|
|
19
|
+
// falls back to the single-header-value for environments that don't
|
|
20
|
+
// expose it. The fallback path only sees the FIRST cookie if multiple
|
|
21
|
+
// were set in one response — a limitation of RFC 7230 headers — which
|
|
22
|
+
// is acceptable here because tests that set multiple cookies run on a
|
|
23
|
+
// runtime that supports getSetCookie.
|
|
24
|
+
export function getSetCookies(res: Response): Map<string, ParsedSetCookie> {
|
|
25
|
+
const getter = (res.headers as { getSetCookie?: () => string[] }).getSetCookie;
|
|
26
|
+
const raws = getter ? getter.call(res.headers) : [res.headers.get("set-cookie") ?? ""];
|
|
27
|
+
const out = new Map<string, ParsedSetCookie>();
|
|
28
|
+
for (const raw of raws) {
|
|
29
|
+
if (!raw) continue;
|
|
30
|
+
const first = raw.split(";")[0];
|
|
31
|
+
if (!first) continue;
|
|
32
|
+
const eq = first.indexOf("=");
|
|
33
|
+
if (eq === -1) continue;
|
|
34
|
+
const name = first.slice(0, eq).trim();
|
|
35
|
+
const value = first.slice(eq + 1).trim();
|
|
36
|
+
out.set(name, { value, raw });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convenience for the common case: "give me the value of cookie X".
|
|
42
|
+
// Returns undefined when the cookie isn't set — no throw, so negative
|
|
43
|
+
// assertions read cleanly (`expect(cookies.get("foo")).toBeUndefined()`).
|
|
44
|
+
export function getSetCookieValue(res: Response, name: string): string | undefined {
|
|
45
|
+
return getSetCookies(res).get(name)?.value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Raw Set-Cookie header for attribute assertions
|
|
49
|
+
// (`expect(raw).toMatch(/SameSite=Lax/)`). Returns undefined when missing.
|
|
50
|
+
export function getSetCookieRaw(res: Response, name: string): string | undefined {
|
|
51
|
+
return getSetCookies(res).get(name)?.raw;
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Test-Assertions, Domain-Test-Fixtures und Vitest-spezifische Helpers.
|
|
2
|
+
// Production-Code (dev-server, bin/) darf NICHTS aus diesem Sub-Path importieren —
|
|
3
|
+
// die Stack-Builder leben in `@cosmicdrift/kumiko-framework/stack`, dieses Modul darf
|
|
4
|
+
// vitest-Imports top-level enthalten (siehe expect-error.ts).
|
|
5
|
+
|
|
6
|
+
export { rolesOf } from "./access-assertions";
|
|
7
|
+
export { expectError, expectSuccess } from "./assertions";
|
|
8
|
+
export {
|
|
9
|
+
type E2EGeneratorOptions,
|
|
10
|
+
type E2ETestSpec,
|
|
11
|
+
type EditFillOp,
|
|
12
|
+
generateE2ESpec,
|
|
13
|
+
generateZodFixture,
|
|
14
|
+
} from "./e2e-generator";
|
|
15
|
+
export { expectErrorIncludes } from "./expect-error";
|
|
16
|
+
export { bridgeStub } from "./handler-context";
|
|
17
|
+
export {
|
|
18
|
+
getSetCookieRaw,
|
|
19
|
+
getSetCookies,
|
|
20
|
+
getSetCookieValue,
|
|
21
|
+
type ParsedSetCookie,
|
|
22
|
+
} from "./http-cookies";
|
|
23
|
+
export { createLateBoundHolder, type LateBoundHolder } from "./late-bound";
|
|
24
|
+
export {
|
|
25
|
+
createMutableMasterKeyProvider,
|
|
26
|
+
type MutableMasterKeyProvider,
|
|
27
|
+
} from "./mutable-master-key-provider";
|
|
28
|
+
export {
|
|
29
|
+
createRecordingProvider,
|
|
30
|
+
type RecordingProvider,
|
|
31
|
+
} from "./observability-recorder";
|
|
32
|
+
export {
|
|
33
|
+
sharedItemEntity,
|
|
34
|
+
sharedItemTable,
|
|
35
|
+
sharedUserEntity,
|
|
36
|
+
sharedUserTable,
|
|
37
|
+
sharedWidgetEntity,
|
|
38
|
+
sharedWidgetTable,
|
|
39
|
+
} from "./shared-entities";
|
|
40
|
+
export { sleep } from "./utils";
|
|
41
|
+
export { waitFor } from "./wait-for";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Late-bound reference for values that only exist AFTER setupTestStack
|
|
2
|
+
// returns — typically the session-callbacks, which close over stack.db.
|
|
3
|
+
//
|
|
4
|
+
// Without this helper, integration tests repeat the same trampoline
|
|
5
|
+
// pattern (let real; async wrapper; null-check on each call). The holder
|
|
6
|
+
// inverts the dependency: the test passes the trampolines into setupTestStack
|
|
7
|
+
// first, then injects the concrete impl once db is available.
|
|
8
|
+
//
|
|
9
|
+
// Production wiring (runDevApp / runProdApp) doesn't reuse this — those
|
|
10
|
+
// wrappers inline an equivalent let-closure-throw pattern to avoid the
|
|
11
|
+
// runtime-isolation cross from dev → test packages. Same idea, different
|
|
12
|
+
// file.
|
|
13
|
+
|
|
14
|
+
export type LateBoundHolder<T> = {
|
|
15
|
+
/** Store the concrete value. Must be called before any trampoline fires. */
|
|
16
|
+
set(value: T): void;
|
|
17
|
+
/** Fetch the concrete value or throw if set() hasn't been called yet. */
|
|
18
|
+
get(): T;
|
|
19
|
+
/** True after set() has been called. */
|
|
20
|
+
isReady(): boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createLateBoundHolder<T>(label = "value"): LateBoundHolder<T> {
|
|
24
|
+
let value: T | undefined;
|
|
25
|
+
return {
|
|
26
|
+
set(v) {
|
|
27
|
+
value = v;
|
|
28
|
+
},
|
|
29
|
+
get() {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
throw new Error(`late-bound ${label} accessed before set() was called`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
},
|
|
35
|
+
isReady() {
|
|
36
|
+
return value !== undefined;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Test utility: a MasterKeyProvider wrapper whose backing provider can be
|
|
2
|
+
// swapped mid-test. Use-case: simulating KEK-rotation ("ops flipped
|
|
3
|
+
// CURRENT=2") without rebuilding the whole stack + SecretsContext.
|
|
4
|
+
//
|
|
5
|
+
// Prod analogue: ENV swap + process restart. This helper is purely for
|
|
6
|
+
// tests that want to exercise pre- and post-rotation behaviour in a
|
|
7
|
+
// single suite.
|
|
8
|
+
|
|
9
|
+
import type { MasterKeyProvider } from "../secrets";
|
|
10
|
+
|
|
11
|
+
export type MutableMasterKeyProvider = MasterKeyProvider & {
|
|
12
|
+
// Replace the backing provider. All future wrapDek/unwrapDek/currentVersion
|
|
13
|
+
// calls delegate to `next`. Existing in-flight calls already hold a
|
|
14
|
+
// reference to the old provider's closure and finish under its contract.
|
|
15
|
+
replace(next: MasterKeyProvider): void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createMutableMasterKeyProvider(
|
|
19
|
+
initial: MasterKeyProvider,
|
|
20
|
+
): MutableMasterKeyProvider {
|
|
21
|
+
let current = initial;
|
|
22
|
+
return {
|
|
23
|
+
wrapDek: (dek) => current.wrapDek(dek),
|
|
24
|
+
unwrapDek: (e, v) => current.unwrapDek(e, v),
|
|
25
|
+
currentVersion: () => current.currentVersion(),
|
|
26
|
+
isAvailable: () => current.isAvailable(),
|
|
27
|
+
replace: (next) => {
|
|
28
|
+
current = next;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SENSITIVE_CONFIG,
|
|
3
|
+
type MetricEvent,
|
|
4
|
+
mergeSensitiveConfig,
|
|
5
|
+
type ObservabilityOptions,
|
|
6
|
+
type ObservabilityProvider,
|
|
7
|
+
type RecordedSpan,
|
|
8
|
+
RecordingMeter,
|
|
9
|
+
RecordingTracer,
|
|
10
|
+
} from "../observability";
|
|
11
|
+
|
|
12
|
+
// Provider that keeps every emitted span + metric event in arrays for
|
|
13
|
+
// assertion in integration tests. Use instead of ConsoleProvider when the
|
|
14
|
+
// test needs to inspect the trace tree or verify metric emissions.
|
|
15
|
+
export type RecordingProvider = ObservabilityProvider & {
|
|
16
|
+
readonly spans: readonly RecordedSpan[];
|
|
17
|
+
readonly metricEvents: readonly MetricEvent[];
|
|
18
|
+
// Returns spans filtered by name — handy for `.find(s => s.name === "http.request")`.
|
|
19
|
+
spansByName(name: string): readonly RecordedSpan[];
|
|
20
|
+
// All spans sharing a trace id — use this to reconstruct a single request's tree.
|
|
21
|
+
spansByTraceId(traceId: string): readonly RecordedSpan[];
|
|
22
|
+
reset(): void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createRecordingProvider(options: ObservabilityOptions = {}): RecordingProvider {
|
|
26
|
+
const sensitiveConfig = mergeSensitiveConfig(options.sensitiveFilter ?? DEFAULT_SENSITIVE_CONFIG);
|
|
27
|
+
const spans: RecordedSpan[] = [];
|
|
28
|
+
const metricEvents: MetricEvent[] = [];
|
|
29
|
+
|
|
30
|
+
const tracer = new RecordingTracer({
|
|
31
|
+
sensitiveConfig,
|
|
32
|
+
onSpanEnd: (s) => spans.push(s),
|
|
33
|
+
});
|
|
34
|
+
const meter = new RecordingMeter((e) => metricEvents.push(e));
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name: "recording",
|
|
38
|
+
tracer,
|
|
39
|
+
meter,
|
|
40
|
+
spans,
|
|
41
|
+
metricEvents,
|
|
42
|
+
spansByName(name: string) {
|
|
43
|
+
return spans.filter((s) => s.name === name);
|
|
44
|
+
},
|
|
45
|
+
spansByTraceId(traceId: string) {
|
|
46
|
+
return spans.filter((s) => s.traceId === traceId);
|
|
47
|
+
},
|
|
48
|
+
reset() {
|
|
49
|
+
spans.length = 0;
|
|
50
|
+
metricEvents.length = 0;
|
|
51
|
+
},
|
|
52
|
+
async shutdown() {},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildDrizzleTable } from "../db/table-builder";
|
|
2
|
+
import {
|
|
3
|
+
createBooleanField,
|
|
4
|
+
createEntity,
|
|
5
|
+
createNumberField,
|
|
6
|
+
createTextField,
|
|
7
|
+
} from "../engine/factories";
|
|
8
|
+
|
|
9
|
+
// --- Shared Entity Fixtures -------------------------------------------------
|
|
10
|
+
//
|
|
11
|
+
// Replaces inline `createEntity(...) + buildDrizzleTable(...)` boilerplate
|
|
12
|
+
// that appeared in 20+ integration tests. Pick the shape closest to what
|
|
13
|
+
// the test needs; if a feature needs extras (hooks, state-machine, fields),
|
|
14
|
+
// keep a local inline entity rather than bloating these shared ones.
|
|
15
|
+
|
|
16
|
+
// "Just a name" — minimal entity with `name: text`, softDelete on.
|
|
17
|
+
// Used by every pipeline test that only needs SOMETHING to write events
|
|
18
|
+
// against (event-dispatcher*, event-retention, event-dedup, …).
|
|
19
|
+
export const sharedWidgetEntity = createEntity({
|
|
20
|
+
fields: { name: createTextField({ required: true }) },
|
|
21
|
+
softDelete: true,
|
|
22
|
+
});
|
|
23
|
+
export const sharedWidgetTable = buildDrizzleTable("widget", sharedWidgetEntity);
|
|
24
|
+
|
|
25
|
+
// User with searchable name/email fields. Used by full-stack, cascade,
|
|
26
|
+
// and any test that exercises search-indexing or field-access on a
|
|
27
|
+
// realistic-looking user record.
|
|
28
|
+
export const sharedUserEntity = createEntity({
|
|
29
|
+
fields: {
|
|
30
|
+
email: createTextField({ required: true, format: "email", searchable: true }),
|
|
31
|
+
firstName: createTextField({ searchable: true }),
|
|
32
|
+
lastName: createTextField({ searchable: true }),
|
|
33
|
+
isEnabled: createBooleanField({ default: true }),
|
|
34
|
+
},
|
|
35
|
+
softDelete: true,
|
|
36
|
+
searchWeight: 10,
|
|
37
|
+
});
|
|
38
|
+
export const sharedUserTable = buildDrizzleTable("user", sharedUserEntity);
|
|
39
|
+
|
|
40
|
+
// Item with name + optional price. Used by error-contract, batch,
|
|
41
|
+
// projection-rebuild — tests that need "a thing you can CRUD".
|
|
42
|
+
export const sharedItemEntity = createEntity({
|
|
43
|
+
fields: {
|
|
44
|
+
name: createTextField({ required: true }),
|
|
45
|
+
price: createNumberField(),
|
|
46
|
+
},
|
|
47
|
+
softDelete: true,
|
|
48
|
+
});
|
|
49
|
+
export const sharedItemTable = buildDrizzleTable("item", sharedItemEntity);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polls a condition with escalating timeouts.
|
|
3
|
+
*
|
|
4
|
+
* Default schedule: 250ms → 1s → 3s (3 attempts).
|
|
5
|
+
* Returns immediately on success. Throws the last assertion error if all attempts fail.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* await waitFor(() => {
|
|
9
|
+
* expect(events).toHaveLength(1);
|
|
10
|
+
* });
|
|
11
|
+
*/
|
|
12
|
+
export async function waitFor(
|
|
13
|
+
fn: () => void | Promise<void>,
|
|
14
|
+
options?: { delays?: number[] },
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const delays = options?.delays ?? [250, 1000, 3000];
|
|
17
|
+
let lastError: unknown;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < delays.length; i++) {
|
|
20
|
+
await new Promise((r) => setTimeout(r, delays[i]));
|
|
21
|
+
try {
|
|
22
|
+
await fn();
|
|
23
|
+
// skip: retry attempt succeeded, no further polling needed
|
|
24
|
+
return;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
lastError = err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw lastError;
|
|
31
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Smoke-Test für die Temporal-Polyfill-Initialisierung.
|
|
2
|
+
// Sicher dass nach ensureTemporalPolyfill() die wichtigsten Temporal-Typen
|
|
3
|
+
// (Instant, PlainDate, ZonedDateTime) konstruktor-fähig sind.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { ensureTemporalPolyfill, getTemporal } from "../polyfill";
|
|
7
|
+
|
|
8
|
+
describe("Temporal Polyfill", () => {
|
|
9
|
+
test("ensureTemporalPolyfill ist idempotent + Temporal nach dreifachem Aufruf nutzbar", async () => {
|
|
10
|
+
await ensureTemporalPolyfill();
|
|
11
|
+
await ensureTemporalPolyfill();
|
|
12
|
+
await ensureTemporalPolyfill();
|
|
13
|
+
// Idempotenz beweisen: nach mehrfachem Aufruf muss Temporal weiterhin
|
|
14
|
+
// konstruktor-fähig sein (kein zerstörter global state, kein verschütteter
|
|
15
|
+
// Singleton).
|
|
16
|
+
const T = getTemporal();
|
|
17
|
+
expect(T.Instant.from("2026-04-18T10:00:00Z").epochMilliseconds).toBe(
|
|
18
|
+
Date.UTC(2026, 3, 18, 10, 0, 0),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("Temporal.Instant ist nach Polyfill konstruktor-fähig", async () => {
|
|
23
|
+
await ensureTemporalPolyfill();
|
|
24
|
+
const T = getTemporal();
|
|
25
|
+
const instant = T.Instant.from("2026-04-18T10:00:00Z");
|
|
26
|
+
expect(instant.toString()).toBe("2026-04-18T10:00:00Z");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Temporal.PlainDate funktioniert", async () => {
|
|
30
|
+
await ensureTemporalPolyfill();
|
|
31
|
+
const T = getTemporal();
|
|
32
|
+
const date = T.PlainDate.from("2026-04-18");
|
|
33
|
+
expect(date.toString()).toBe("2026-04-18");
|
|
34
|
+
expect(date.year).toBe(2026);
|
|
35
|
+
expect(date.month).toBe(4);
|
|
36
|
+
expect(date.day).toBe(18);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("Temporal.ZonedDateTime mit IANA-Zone (Europe/Lisbon) funktioniert", async () => {
|
|
40
|
+
await ensureTemporalPolyfill();
|
|
41
|
+
const T = getTemporal();
|
|
42
|
+
const zdt = T.ZonedDateTime.from("2026-04-18T10:00:00[Europe/Lisbon]");
|
|
43
|
+
expect(zdt.timeZoneId).toBe("Europe/Lisbon");
|
|
44
|
+
expect(zdt.hour).toBe(10);
|
|
45
|
+
// Lisbon ist im April auf WEST (UTC+1 wegen DST), deshalb 09:00 UTC.
|
|
46
|
+
expect(zdt.toInstant().toString()).toBe("2026-04-18T09:00:00Z");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("DST-Übergang 2026-03-29 02:30 Europe/Berlin existiert nicht", async () => {
|
|
50
|
+
await ensureTemporalPolyfill();
|
|
51
|
+
const T = getTemporal();
|
|
52
|
+
// Deutschland Spring-Forward 2026: 02:00 → 03:00 in der Nacht 28→29 März.
|
|
53
|
+
// 02:30 existiert NICHT in Berlin-TZ. Temporal handhabt das via
|
|
54
|
+
// `disambiguation: "reject"` korrekt.
|
|
55
|
+
expect(() =>
|
|
56
|
+
T.ZonedDateTime.from(
|
|
57
|
+
{ year: 2026, month: 3, day: 29, hour: 2, minute: 30, timeZone: "Europe/Berlin" },
|
|
58
|
+
{ disambiguation: "reject" },
|
|
59
|
+
),
|
|
60
|
+
).toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("getTemporal vor Polyfill-Init throwt wenn Native fehlt", () => {
|
|
64
|
+
// Note: in unserem Test-Run ist Temporal nach dem ersten ensureTemporalPolyfill()
|
|
65
|
+
// schon installiert (Module-Singleton). Daher prüfen wir das `throws`-Verhalten
|
|
66
|
+
// nur indirekt über die Implementierung — direkter Test wäre flaky weil
|
|
67
|
+
// global state geteilt wird.
|
|
68
|
+
// Stattdessen: einfach sicherstellen dass nach Init getTemporal nicht throwt.
|
|
69
|
+
const T = getTemporal();
|
|
70
|
+
expect(T).toBeDefined();
|
|
71
|
+
expect(typeof T.Instant.from).toBe("function");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Unit-Tests für ctx.tz API.
|
|
2
|
+
// Test-Fokus: korrekte Konvertierung Wall-Clock+TZ ↔ Instant ↔ JSON-Pair.
|
|
3
|
+
|
|
4
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
5
|
+
import { ensureTemporalPolyfill } from "../polyfill";
|
|
6
|
+
import { createTzContext } from "../tz-context";
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await ensureTemporalPolyfill();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("ctx.tz — defaults", () => {
|
|
13
|
+
test("ohne Options: tenant + user beide UTC", () => {
|
|
14
|
+
const tz = createTzContext();
|
|
15
|
+
expect(tz.tenant).toBe("UTC");
|
|
16
|
+
expect(tz.user).toBe("UTC");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("nur tenant gesetzt: user fällt auf tenant zurück", () => {
|
|
20
|
+
const tz = createTzContext({ tenant: "Europe/Berlin" });
|
|
21
|
+
expect(tz.tenant).toBe("Europe/Berlin");
|
|
22
|
+
expect(tz.user).toBe("Europe/Berlin");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("user-Override sticht tenant", () => {
|
|
26
|
+
const tz = createTzContext({ tenant: "Europe/Berlin", user: "Asia/Tokyo" });
|
|
27
|
+
expect(tz.tenant).toBe("Europe/Berlin");
|
|
28
|
+
expect(tz.user).toBe("Asia/Tokyo");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("ctx.tz — now / today", () => {
|
|
33
|
+
test("now() liefert Temporal.Instant", () => {
|
|
34
|
+
const tz = createTzContext();
|
|
35
|
+
const instant = tz.now();
|
|
36
|
+
expect(typeof instant.epochMilliseconds).toBe("number");
|
|
37
|
+
// Sollte nahe an aktueller Wall-Time sein (innerhalb 5 Sekunden).
|
|
38
|
+
expect(Math.abs(instant.epochMilliseconds - Date.now())).toBeLessThan(5000);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("nowIn(tz) liefert ZonedDateTime in der richtigen Zone", () => {
|
|
42
|
+
const tz = createTzContext();
|
|
43
|
+
const zdt = tz.nowIn("Europe/Berlin");
|
|
44
|
+
expect(zdt.timeZoneId).toBe("Europe/Berlin");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("today(tz) liefert PlainDate ohne Zeit-Komponente", () => {
|
|
48
|
+
const tz = createTzContext();
|
|
49
|
+
const today = tz.today("Europe/Lisbon");
|
|
50
|
+
// PlainDate hat keine .hour-Property — wenn es eines hätte, wäre's ein
|
|
51
|
+
// ZonedDateTime und der Test würde TypeError werfen.
|
|
52
|
+
expect("hour" in today).toBe(false);
|
|
53
|
+
expect(today.toString()).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("todayRange(tz) liefert UTC-Instants für DB-Range-Query", () => {
|
|
57
|
+
const tz = createTzContext();
|
|
58
|
+
const range = tz.todayRange("Europe/Berlin");
|
|
59
|
+
// Differenz zwischen start + end ist genau 24h (oder 23/25h bei DST).
|
|
60
|
+
const diffHours =
|
|
61
|
+
(range.end.epochMilliseconds - range.start.epochMilliseconds) / (1000 * 60 * 60);
|
|
62
|
+
expect(diffHours).toBeGreaterThanOrEqual(23);
|
|
63
|
+
expect(diffHours).toBeLessThanOrEqual(25);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("ctx.tz — parse + JSON-Pair Konvertierungen", () => {
|
|
68
|
+
test("parse(wallClock, tz) liefert ZonedDateTime mit korrekter UTC-Konvertierung", () => {
|
|
69
|
+
const tz = createTzContext();
|
|
70
|
+
// 2026-04-03 10:00 Lisbon = 09:00 UTC (WEST = UTC+1)
|
|
71
|
+
const zdt = tz.parse("2026-04-03T10:00:00", "Europe/Lisbon");
|
|
72
|
+
expect(zdt.timeZoneId).toBe("Europe/Lisbon");
|
|
73
|
+
expect(zdt.toInstant().toString()).toBe("2026-04-03T09:00:00Z");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("toLocatedJson(zdt) liefert { at, tz } OHNE Offset-Marker", () => {
|
|
77
|
+
const tz = createTzContext();
|
|
78
|
+
const zdt = tz.parse("2026-04-03T10:00:00", "Europe/Lisbon");
|
|
79
|
+
const json = tz.toLocatedJson(zdt);
|
|
80
|
+
expect(json).toEqual({ at: "2026-04-03T10:00:00", tz: "Europe/Lisbon" });
|
|
81
|
+
// Kein "Z", kein "+01:00" im at-Feld — sonst ist die JSON-Form nicht
|
|
82
|
+
// mehr idiotensicher.
|
|
83
|
+
expect(json.at).not.toContain("Z");
|
|
84
|
+
expect(json.at).not.toContain("+");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("fromLocatedJson({ at, tz }) ist Inverse zu toLocatedJson", () => {
|
|
88
|
+
const tz = createTzContext();
|
|
89
|
+
const original = tz.parse("2026-04-03T10:00:00", "Europe/Lisbon");
|
|
90
|
+
const json = tz.toLocatedJson(original);
|
|
91
|
+
const restored = tz.fromLocatedJson(json);
|
|
92
|
+
expect(restored.toInstant().equals(original.toInstant())).toBe(true);
|
|
93
|
+
expect(restored.timeZoneId).toBe(original.timeZoneId);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("Round-Trip funktioniert über DST-Übergang (Lisbon-Sommerzeit-Ende)", () => {
|
|
97
|
+
const tz = createTzContext();
|
|
98
|
+
// 2026-10-25 in Lisbon: Fall-Back von 02:00 → 01:00. Wir nehmen 14:00,
|
|
99
|
+
// was eindeutig im WET ist (UTC+0).
|
|
100
|
+
const zdt = tz.parse("2026-10-25T14:00:00", "Europe/Lisbon");
|
|
101
|
+
const json = tz.toLocatedJson(zdt);
|
|
102
|
+
expect(json.at).toBe("2026-10-25T14:00:00");
|
|
103
|
+
expect(json.tz).toBe("Europe/Lisbon");
|
|
104
|
+
const restored = tz.fromLocatedJson(json);
|
|
105
|
+
// Nach DST-Wechsel ist Lisbon UTC+0 → 14:00 lokal = 14:00 UTC.
|
|
106
|
+
expect(restored.toInstant().toString()).toBe("2026-10-25T14:00:00Z");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("ctx.tz — Cross-Server-TZ Garantie", () => {
|
|
111
|
+
test("derselbe Wall-Clock+TZ liefert denselben UTC-Instant unabhängig vom Server-TZ", () => {
|
|
112
|
+
// Das ist der Kern der Migration: ein Server in Berlin und einer in
|
|
113
|
+
// Tokyo schicken denselben "Pickup 10:00 Lissabon" — beide müssen den
|
|
114
|
+
// gleichen UTC-Instant produzieren. Temporal erfüllt das per Design,
|
|
115
|
+
// wir prüfen es zur Sicherheit.
|
|
116
|
+
const tz = createTzContext();
|
|
117
|
+
const zdt1 = tz.parse("2026-04-03T10:00:00", "Europe/Lisbon");
|
|
118
|
+
const zdt2 = tz.fromLocatedJson({ at: "2026-04-03T10:00:00", tz: "Europe/Lisbon" });
|
|
119
|
+
expect(zdt1.toInstant().equals(zdt2.toInstant())).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Time-Modul: Temporal-Polyfill + ctx.tz Helper.
|
|
2
|
+
//
|
|
3
|
+
// Aktueller Stand (Iteration 1-4 von Gap-03 in samples/beammycar/MIGRATION.md):
|
|
4
|
+
// - ensureTemporalPolyfill: installiert Temporal global wenn nötig
|
|
5
|
+
// - getTemporal: type-safer Zugriff auf globalThis.Temporal
|
|
6
|
+
// - createTzContext: ctx.tz Factory mit now/today/parse/toLocatedJson/...
|
|
7
|
+
// - LocatedTimestampJson: API-Boundary-Form { at, tz }
|
|
8
|
+
//
|
|
9
|
+
// Kommt:
|
|
10
|
+
// - DB-Wrapper (Wall-Clock+tz ↔ UTC transparent in Drizzle-Layer)
|
|
11
|
+
// - Lint-Regel "kein new Date() im Feature-Code"
|
|
12
|
+
// - UI-Komponenten <DateTimeInput>, <LocatedDateTimePicker>, <DateInput>
|
|
13
|
+
|
|
14
|
+
export { ensureTemporalPolyfill, getTemporal } from "./polyfill";
|
|
15
|
+
export {
|
|
16
|
+
createTzContext,
|
|
17
|
+
createTzContextAsync,
|
|
18
|
+
type LocatedTimestampJson,
|
|
19
|
+
type TzContext,
|
|
20
|
+
type TzContextOptions,
|
|
21
|
+
} from "./tz-context";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Temporal-Polyfill-Initialisierung.
|
|
2
|
+
//
|
|
3
|
+
// Hintergrund: Temporal ist seit Anfang 2026 in Chromium 144+ und Firefox 139+
|
|
4
|
+
// nativ verfügbar, aber nicht in Safari, iOS, oder Hermes (React Native).
|
|
5
|
+
// Bun/Node haben es teilweise (V8-abhängig, instabil).
|
|
6
|
+
//
|
|
7
|
+
// Damit kumiko-Apps universal laufen — Server (Bun), Web (alle Browser),
|
|
8
|
+
// Mobile (Hermes) — installiert das Framework beim Boot einmal den
|
|
9
|
+
// `temporal-polyfill` (FullCalendar, ~25 KB). Auf Runtimes mit nativem
|
|
10
|
+
// Temporal ist der Aufruf ein No-Op.
|
|
11
|
+
//
|
|
12
|
+
// Idempotent: mehrfacher Aufruf ist sicher (Polyfill prüft selbst ob
|
|
13
|
+
// `globalThis.Temporal` schon existiert). Wir cachen das Ergebnis trotzdem
|
|
14
|
+
// in einem Modul-Singleton, damit Boot-Performance nicht jedes Mal das
|
|
15
|
+
// Polyfill-Modul-Loading triggert.
|
|
16
|
+
|
|
17
|
+
let polyfillInstalled = false;
|
|
18
|
+
let polyfillPromise: Promise<void> | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stellt sicher dass `globalThis.Temporal` verfügbar ist. Idempotent.
|
|
22
|
+
*
|
|
23
|
+
* - Wenn Native Temporal existiert (moderne Browser, neueres Bun): No-Op.
|
|
24
|
+
* - Sonst: lädt `temporal-polyfill/global` (installiert globalThis.Temporal).
|
|
25
|
+
*
|
|
26
|
+
* Wird einmal beim Framework-Boot aufgerufen. Feature-Code muss das nicht
|
|
27
|
+
* selbst tun — `Temporal` ist nach dem Boot überall verfügbar.
|
|
28
|
+
*/
|
|
29
|
+
export async function ensureTemporalPolyfill(): Promise<void> {
|
|
30
|
+
// skip: Idempotenz — Polyfill bereits installiert in einem früheren Aufruf.
|
|
31
|
+
if (polyfillInstalled) return;
|
|
32
|
+
if (polyfillPromise) {
|
|
33
|
+
await polyfillPromise;
|
|
34
|
+
// skip: Concurrent-Boot — anderer Aufruf hat die Installation übernommen.
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
polyfillPromise = (async () => {
|
|
39
|
+
// Runtime-Check entgegen Type-System-Annahme: temporal-spec deklariert
|
|
40
|
+
// Temporal ambient, aber ohne Polyfill fehlt es zur Laufzeit. `in`-Check
|
|
41
|
+
// ist die sauberste Prüfung, ohne Cast + ohne TS-Truthy-Warnings.
|
|
42
|
+
if ("Temporal" in globalThis) {
|
|
43
|
+
polyfillInstalled = true;
|
|
44
|
+
// skip: Native Temporal vorhanden — Polyfill nicht nötig.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Polyfill globally installieren — der Side-Effect-Import setzt
|
|
48
|
+
// globalThis.Temporal.
|
|
49
|
+
await import("temporal-polyfill/global");
|
|
50
|
+
polyfillInstalled = true;
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
await polyfillPromise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Type-safe Zugriff auf globalThis.Temporal. Wirft wenn der Polyfill noch
|
|
58
|
+
* nicht installiert ist (Boot-Reihenfolge-Bug). Feature-Code sollte
|
|
59
|
+
* `ensureTemporalPolyfill()` einmal awaiten und danach `Temporal` global
|
|
60
|
+
* nutzen, oder über diesen Helper die Sicherheit haben.
|
|
61
|
+
*/
|
|
62
|
+
export function getTemporal(): typeof Temporal {
|
|
63
|
+
// Runtime-Check entgegen Type-System-Annahme — siehe ensureTemporalPolyfill.
|
|
64
|
+
if (!("Temporal" in globalThis)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Temporal not available. Call ensureTemporalPolyfill() during framework boot before any time-related code runs.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return Temporal;
|
|
70
|
+
}
|