@cosmicdrift/kumiko-dev-server 0.328.1 → 0.330.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 (125) hide show
  1. package/README.md +17 -0
  2. package/bin/kumiko-build.ts +3 -2
  3. package/bin/kumiko-dev.ts +1 -1
  4. package/bin/kumiko-init-deploy.ts +1 -1
  5. package/bin/kumiko-schema-check.ts +4 -1
  6. package/dist/build-server-bundle.d.ts +31 -0
  7. package/dist/build-server-bundle.js +282 -0
  8. package/dist/build.d.ts +2 -0
  9. package/dist/build.js +5 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/codegen/index.d.ts +4 -0
  13. package/dist/codegen/index.js +4 -0
  14. package/dist/codegen/render.d.ts +55 -0
  15. package/dist/codegen/render.js +273 -0
  16. package/dist/codegen/run-codegen.d.ts +20 -0
  17. package/dist/codegen/run-codegen.js +219 -0
  18. package/dist/codegen/scan-events.d.ts +76 -0
  19. package/dist/codegen/scan-events.js +447 -0
  20. package/dist/codegen/watch.d.ts +33 -0
  21. package/dist/codegen/watch.js +112 -0
  22. package/dist/compose-stacks.d.ts +60 -0
  23. package/dist/compose-stacks.js +127 -0
  24. package/dist/crash-tracker.d.ts +17 -0
  25. package/dist/crash-tracker.js +37 -0
  26. package/dist/create-kumiko-server.d.ts +195 -0
  27. package/dist/create-kumiko-server.js +1040 -0
  28. package/dist/env-schema.d.ts +15 -0
  29. package/dist/env-schema.js +60 -0
  30. package/dist/few-shot-corpus.d.ts +72 -0
  31. package/dist/few-shot-corpus.js +262 -0
  32. package/dist/framework-version.d.ts +1 -0
  33. package/dist/framework-version.js +31 -0
  34. package/dist/index.d.ts +22 -0
  35. package/dist/index.js +33 -0
  36. package/dist/init-deploy-cli.d.ts +10 -0
  37. package/dist/init-deploy-cli.js +111 -0
  38. package/dist/kebab.d.ts +1 -0
  39. package/{src/kebab.ts → dist/kebab.js} +2 -3
  40. package/dist/run-dev-app.d.ts +174 -0
  41. package/dist/run-dev-app.js +403 -0
  42. package/dist/scaffold-app-feature.d.ts +16 -0
  43. package/dist/scaffold-app-feature.js +240 -0
  44. package/dist/scaffold-app.d.ts +45 -0
  45. package/dist/scaffold-app.js +852 -0
  46. package/dist/scaffold-demo-tasks.d.ts +8 -0
  47. package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
  48. package/dist/scaffold-deploy.d.ts +92 -0
  49. package/dist/scaffold-deploy.js +254 -0
  50. package/dist/scaffold-feature.d.ts +26 -0
  51. package/dist/scaffold-feature.js +163 -0
  52. package/dist/schema-apply.d.ts +11 -0
  53. package/dist/schema-apply.js +95 -0
  54. package/dist/schema-check-core.d.ts +2 -0
  55. package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
  56. package/dist/schema-tables-auth-mode.d.ts +2 -0
  57. package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
  58. package/dist/schema-tables-minimal.d.ts +2 -0
  59. package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
  60. package/dist/setup-test-stack-from-features.d.ts +11 -0
  61. package/dist/setup-test-stack-from-features.js +42 -0
  62. package/dist/welcome-banner.d.ts +15 -0
  63. package/dist/welcome-banner.js +27 -0
  64. package/package.json +71 -24
  65. package/src/changes.json +6 -0
  66. package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
  67. package/src/__tests__/build-server-bundle.test.ts +0 -228
  68. package/src/__tests__/classify-change.test.ts +0 -87
  69. package/src/__tests__/compose-stacks.test.ts +0 -273
  70. package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
  71. package/src/__tests__/crash-tracker.test.ts +0 -89
  72. package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
  73. package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
  74. package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
  75. package/src/__tests__/discover-format.test.ts +0 -32
  76. package/src/__tests__/env-schema.integration.test.ts +0 -248
  77. package/src/__tests__/env-schema.test.ts +0 -132
  78. package/src/__tests__/few-shot-corpus.test.ts +0 -316
  79. package/src/__tests__/framework-version.test.ts +0 -45
  80. package/src/__tests__/init-deploy-cli.test.ts +0 -113
  81. package/src/__tests__/kumiko-schema-check.test.ts +0 -56
  82. package/src/__tests__/merge-extra-context.test.ts +0 -67
  83. package/src/__tests__/resolve-stylesheet.test.ts +0 -132
  84. package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
  85. package/src/__tests__/run-dev-app.integration.test.ts +0 -290
  86. package/src/__tests__/runtime-classification.test.ts +0 -26
  87. package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
  88. package/src/__tests__/scaffold-app-feature.test.ts +0 -216
  89. package/src/__tests__/scaffold-app.test.ts +0 -630
  90. package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
  91. package/src/__tests__/scaffold-deploy.test.ts +0 -464
  92. package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
  93. package/src/__tests__/scaffold-feature.test.ts +0 -143
  94. package/src/__tests__/schema-apply.integration.test.ts +0 -191
  95. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
  96. package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
  97. package/src/__tests__/walkthrough.integration.test.ts +0 -140
  98. package/src/__tests__/welcome-banner.test.ts +0 -42
  99. package/src/build-server-bundle.ts +0 -346
  100. package/src/build.ts +0 -28
  101. package/src/codegen/__tests__/render-codegen.test.ts +0 -100
  102. package/src/codegen/__tests__/run-codegen.test.ts +0 -665
  103. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
  104. package/src/codegen/__tests__/watch.test.ts +0 -222
  105. package/src/codegen/index.ts +0 -19
  106. package/src/codegen/render.ts +0 -305
  107. package/src/codegen/run-codegen.ts +0 -278
  108. package/src/codegen/scan-events.ts +0 -565
  109. package/src/codegen/watch.ts +0 -140
  110. package/src/compose-stacks.ts +0 -193
  111. package/src/crash-tracker.ts +0 -56
  112. package/src/create-kumiko-server.ts +0 -1397
  113. package/src/env-schema.ts +0 -72
  114. package/src/few-shot-corpus.ts +0 -370
  115. package/src/framework-version.ts +0 -39
  116. package/src/index.ts +0 -105
  117. package/src/init-deploy-cli.ts +0 -148
  118. package/src/run-dev-app.ts +0 -694
  119. package/src/scaffold-app-feature.ts +0 -283
  120. package/src/scaffold-app.ts +0 -1033
  121. package/src/scaffold-deploy.ts +0 -393
  122. package/src/scaffold-feature.ts +0 -226
  123. package/src/schema-apply.ts +0 -123
  124. package/src/setup-test-stack-from-features.ts +0 -80
  125. package/src/welcome-banner.ts +0 -43
@@ -1,273 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
3
- import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
4
- import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
5
- import {
6
- composeFileStack,
7
- composeGdprStack,
8
- composeIdentityStack,
9
- composeMailStack,
10
- composeOpsStack,
11
- composePagesStack,
12
- composeRendererStack,
13
- composeUserDataRightsStack,
14
- stackFeatureNames,
15
- } from "../compose-stacks";
16
-
17
- const TEST_MFA = {
18
- setupTokenSecret: "compose-stacks-mfa-setup-secret-at-least-32b!!",
19
- challengeTokenSecret: "compose-stacks-mfa-challenge-secret-32b!!",
20
- issuer: "Kumiko ComposeStacks",
21
- } as const;
22
-
23
- describe("composeStacks", () => {
24
- test("composeRendererStack → template-resolver, renderer-foundation, renderer-simple", () => {
25
- expect(stackFeatureNames(composeRendererStack())).toEqual([
26
- "template-resolver",
27
- "renderer-foundation",
28
- "renderer-simple",
29
- ]);
30
- });
31
-
32
- test("composePagesStack → legal-pages (template-resolver comes from the renderer stack)", () => {
33
- expect(stackFeatureNames(composePagesStack())).toEqual(["legal-pages"]);
34
- });
35
-
36
- test("composeMailStack mounts foundation + selected transports", () => {
37
- expect(stackFeatureNames(composeMailStack({ transports: ["inmemory", "smtp"] }))).toEqual([
38
- "mail-foundation",
39
- "mail-transport-inmemory",
40
- "mail-transport-smtp",
41
- ]);
42
- });
43
-
44
- test("composeFileStack mounts foundation + providers + files entity", () => {
45
- expect(stackFeatureNames(composeFileStack({ providers: ["inmemory", "s3"] }))).toEqual([
46
- "file-foundation",
47
- "file-provider-inmemory",
48
- "file-provider-s3",
49
- "files",
50
- ]);
51
- });
52
-
53
- test("composeGdprStack retention-first (money-horse / publicstatus pattern)", () => {
54
- expect(stackFeatureNames(composeGdprStack({ tenantLifecycle: true, sessions: true }))).toEqual([
55
- "data-retention",
56
- "compliance-profiles",
57
- "tenant-lifecycle",
58
- "auth-foundation",
59
- "sessions",
60
- ]);
61
- });
62
-
63
- test("composeGdprStack compliance-first (studio pattern)", () => {
64
- expect(stackFeatureNames(composeGdprStack({ order: "compliance-first" }))).toEqual([
65
- "compliance-profiles",
66
- "data-retention",
67
- ]);
68
- });
69
-
70
- test("composeUserDataRightsStack → udr + defaults", () => {
71
- expect(stackFeatureNames(composeUserDataRightsStack())).toEqual([
72
- "user-data-rights",
73
- "user-data-rights-defaults",
74
- ]);
75
- });
76
-
77
- test("composeOpsStack defaults delivery + audit + jobs", () => {
78
- expect(stackFeatureNames(composeOpsStack())).toEqual(["delivery", "audit", "jobs"]);
79
- });
80
-
81
- test("composeOpsStack rateLimiting opt-in", () => {
82
- expect(stackFeatureNames(composeOpsStack({ rateLimiting: true }))).toEqual([
83
- "delivery",
84
- "audit",
85
- "jobs",
86
- "rate-limiting",
87
- ]);
88
- });
89
-
90
- test("composeIdentityStack defaults auth-foundation + sessions", () => {
91
- expect(stackFeatureNames(composeIdentityStack())).toEqual(["auth-foundation", "sessions"]);
92
- });
93
-
94
- test("composeIdentityStack mounts auth-mfa when mfa options given", () => {
95
- expect(stackFeatureNames(composeIdentityStack({ mfa: TEST_MFA }))).toEqual([
96
- "auth-foundation",
97
- "sessions",
98
- "auth-mfa",
99
- ]);
100
- });
101
-
102
- test("composeIdentityStack sessions:false + mfa → auth-mfa only", () => {
103
- expect(stackFeatureNames(composeIdentityStack({ sessions: false, mfa: TEST_MFA }))).toEqual([
104
- "auth-mfa",
105
- ]);
106
- });
107
-
108
- test("composeIdentityStack + composeGdprStack(sessions) → identical sessions deduped at boot", () => {
109
- const names = stackFeatureNames([
110
- ...composeIdentityStack(),
111
- ...composeGdprStack({ sessions: true }),
112
- ]);
113
- expect(names.filter((n) => n === "sessions")).toHaveLength(2);
114
- const composed = composeFeatures(
115
- [
116
- createPersonalAccessTokensFeature({ scopes: {} }),
117
- ...composeIdentityStack(),
118
- ...composeGdprStack({ sessions: true }),
119
- ],
120
- { includeBundled: true },
121
- );
122
- expect(stackFeatureNames(composed).filter((n) => n === "sessions")).toHaveLength(1);
123
- expect(() => validateBoot(composed)).not.toThrow();
124
- });
125
- });
126
-
127
- /** Snapshot, not a live parity check: intended block combinations, mirrored by hand from studio/money-horse/publicstatus run-configs. A drifted run-config stays green here. */
128
- describe("composeStacks boots for real", () => {
129
- test("studio-shaped combined stack passes validateBoot (not just name-list comparison)", () => {
130
- const features = composeFeatures(
131
- [
132
- createPersonalAccessTokensFeature({ scopes: {} }),
133
- ...composeOpsStack({ rateLimiting: true }),
134
- ...composePagesStack(),
135
- // legal-pages requires template-resolver, which ships in the renderer stack.
136
- ...composeRendererStack(),
137
- ...composeMailStack({ transports: ["inmemory"] }),
138
- ...composeFileStack({ providers: ["inmemory"] }),
139
- ...composeGdprStack({ order: "compliance-first", sessions: true }),
140
- ...composeUserDataRightsStack(),
141
- ],
142
- { includeBundled: true },
143
- );
144
- expect(() => validateBoot(features)).not.toThrow();
145
- });
146
-
147
- test("money-horse-shaped composeGdprStack({sessions:true}) without a tokenVerifier provider passes boot", () => {
148
- // Session-only stacks (#1570): composeGdprStack({sessions:true}) mounts
149
- // auth-foundation + sessions (sessionStore). Cookie auth does not need a
150
- // tokenVerifier; money-horse/solon run-configs pass none and must boot.
151
- const features = composeFeatures([...composeGdprStack({ sessions: true })], {
152
- includeBundled: true,
153
- });
154
- expect(() => validateBoot(features)).not.toThrow();
155
- });
156
-
157
- test("composeGdprStack({sessions:true, providers:[...]}) passes boot with a tokenVerifier mounted", () => {
158
- const features = composeFeatures(
159
- [
160
- ...composeGdprStack({
161
- sessions: true,
162
- providers: [createPersonalAccessTokensFeature({ scopes: {} })],
163
- }),
164
- ],
165
- { includeBundled: true },
166
- );
167
- expect(() => validateBoot(features)).not.toThrow();
168
- });
169
-
170
- // auth-mfa's encrypted fields need a runtime KEK (env or configureEntityFieldEncryption
171
- // + secrets). Name-list + saas-identity-wire.integration.test.ts cover identity boot.
172
- test("identity stack names compose with ops + renderer under includeBundled", () => {
173
- const features = composeFeatures(
174
- [
175
- ...composeIdentityStack({ mfa: TEST_MFA }),
176
- ...composeOpsStack(),
177
- ...composeRendererStack(),
178
- ...composeMailStack({ transports: ["inmemory"] }),
179
- ],
180
- { includeBundled: true },
181
- );
182
- const names = stackFeatureNames(features);
183
- expect(names).toContain("auth-email-password");
184
- expect(names).toContain("sessions");
185
- expect(names).toContain("auth-mfa");
186
- expect(names).toContain("delivery");
187
- });
188
- });
189
-
190
- describe("composeStacks intended block names (snapshot, not live parity)", () => {
191
- test("studio SaaS blocks are covered by presets", () => {
192
- const names = stackFeatureNames([
193
- ...composeOpsStack({ rateLimiting: true }),
194
- ...composePagesStack(),
195
- // Pages content sits on template-resolver, which the renderer stack mounts.
196
- ...composeRendererStack(),
197
- ...composeMailStack({ transports: ["inmemory", "smtp"] }),
198
- ...composeFileStack({ providers: ["inmemory", "s3"] }),
199
- ...composeGdprStack({ order: "compliance-first" }),
200
- ...composeUserDataRightsStack(),
201
- ]);
202
- for (const expected of [
203
- "delivery",
204
- "audit",
205
- "jobs",
206
- "rate-limiting",
207
- "template-resolver",
208
- "legal-pages",
209
- "mail-foundation",
210
- "file-foundation",
211
- "files",
212
- "compliance-profiles",
213
- "data-retention",
214
- "user-data-rights",
215
- "user-data-rights-defaults",
216
- ]) {
217
- expect(names).toContain(expected);
218
- }
219
- });
220
-
221
- test("money-horse GDPR + file block names", () => {
222
- const names = stackFeatureNames([
223
- ...composePagesStack(),
224
- ...composeRendererStack(),
225
- ...composeOpsStack({ delivery: true, audit: false, jobs: false }),
226
- ...composeGdprStack({ sessions: true }),
227
- ...composeFileStack({ providers: ["s3-env"] }),
228
- ...composeUserDataRightsStack(),
229
- ]);
230
- for (const expected of [
231
- "template-resolver",
232
- "legal-pages",
233
- "data-retention",
234
- "compliance-profiles",
235
- "sessions",
236
- "file-foundation",
237
- "file-provider-s3-env",
238
- "files",
239
- "user-data-rights",
240
- ]) {
241
- expect(names).toContain(expected);
242
- }
243
- });
244
-
245
- test("publicstatus core stack block names", () => {
246
- const names = stackFeatureNames([
247
- ...composePagesStack(),
248
- ...composeGdprStack({ tenantLifecycle: true }),
249
- ...composeOpsStack({ delivery: true, audit: true, jobs: true }),
250
- ...composeRendererStack(),
251
- ...composeFileStack({ providers: ["inmemory"] }),
252
- ...composeUserDataRightsStack(),
253
- ]);
254
- for (const expected of [
255
- "template-resolver",
256
- "legal-pages",
257
- "tenant-lifecycle",
258
- "audit",
259
- "delivery",
260
- "jobs",
261
- "renderer-simple",
262
- "files",
263
- ]) {
264
- expect(names).toContain(expected);
265
- }
266
- });
267
-
268
- test("identity stack covers sessions + auth-mfa", () => {
269
- const names = stackFeatureNames(composeIdentityStack({ mfa: TEST_MFA }));
270
- expect(names).toContain("sessions");
271
- expect(names).toContain("auth-mfa");
272
- });
273
- });
@@ -1,165 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import {
3
- configValuesTable,
4
- createConfigAccessorFactory,
5
- createConfigFeature,
6
- createConfigResolver,
7
- } from "@cosmicdrift/kumiko-bundled-features/config";
8
- import {
9
- access,
10
- createTenantConfig,
11
- defineFeature,
12
- defineQueryHandler,
13
- } from "@cosmicdrift/kumiko-framework/engine";
14
- import {
15
- setupTestStack,
16
- type TestStack,
17
- TestUsers,
18
- unsafePushTables,
19
- } from "@cosmicdrift/kumiko-framework/stack";
20
- import * as z from "zod";
21
- import { mergeConfigResolverDefault } from "../run-dev-app";
22
-
23
- // Pins runDevApp's ENV→config-app-override wiring: mergeConfigResolverDefault
24
- // builds the auth-mode configResolver-default with the ENV bridge (a key with
25
- // `env:` gets its env value as the app-override default, symmetric to
26
- // runProdApp), the envSource is injected (never the real process.env), and a
27
- // caller-supplied configResolver still overrides the default.
28
-
29
- const PAGE_SIZE = "devcfg:config:page-size";
30
-
31
- // Reads ctx.config directly — the exact consumption seam mergeConfigResolverDefault
32
- // wires the dispatcher through. Every other test in this file inspects the returned
33
- // {configResolver, _configAccessorFactory} object without ever dispatching a handler
34
- // that actually reads ctx.config; a "Boot writes field A, dispatcher expects field B"
35
- // drift (the original #359-adjacent bug class) would be invisible to all of them.
36
- const readPageSizeQuery = defineQueryHandler({
37
- name: "read-page-size",
38
- schema: z.object({}),
39
- access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
40
- handler: async (_query, ctx) => ({ pageSize: await ctx.config?.(PAGE_SIZE) }),
41
- });
42
-
43
- const devcfgFeature = defineFeature("devcfg", (r) => {
44
- r.requires("config");
45
- r.config({
46
- keys: {
47
- pageSize: createTenantConfig("number", {
48
- env: "DEVCFG_PAGE_SIZE",
49
- default: 10,
50
- read: access.all,
51
- write: access.all,
52
- }),
53
- },
54
- });
55
- r.queryHandler(readPageSizeQuery);
56
- });
57
-
58
- // ctx=undefined → object form, configResolver is the only key (test boundary).
59
- type ExtraObj = { configResolver: ReturnType<typeof createConfigResolver> };
60
-
61
- let stack: TestStack;
62
-
63
- beforeAll(async () => {
64
- stack = await setupTestStack({
65
- features: [createConfigFeature(), devcfgFeature],
66
- extraContext: ({ registry }) => {
67
- const bootResolver = createConfigResolver();
68
- return {
69
- configResolver: bootResolver,
70
- _configAccessorFactory: createConfigAccessorFactory(registry, bootResolver),
71
- };
72
- },
73
- });
74
- await unsafePushTables(stack.db, { configValuesTable });
75
- });
76
-
77
- afterAll(async () => {
78
- await stack.cleanup();
79
- });
80
-
81
- function resolverFor(envSource: Record<string, string | undefined>) {
82
- const extra = mergeConfigResolverDefault(undefined, stack.registry, envSource) as ExtraObj;
83
- return extra.configResolver;
84
- }
85
-
86
- function peekPageSize(resolver: ReturnType<typeof createConfigResolver>) {
87
- const keyDef = stack.registry.getConfigKey(PAGE_SIZE);
88
- if (!keyDef) throw new Error("page-size key missing from registry");
89
- return resolver.get(
90
- PAGE_SIZE,
91
- keyDef,
92
- TestUsers.systemAdmin.tenantId,
93
- TestUsers.systemAdmin.id,
94
- stack.db,
95
- );
96
- }
97
-
98
- describe("runDevApp configResolver-default — ENV→app-override bridge", () => {
99
- test("a key with `env:` resolves to the injected env value as app-override", async () => {
100
- const value = await peekPageSize(resolverFor({ DEVCFG_PAGE_SIZE: "25" }));
101
- expect(value).toBe(25); // number, coerced from env — not 10 (keyDef.default)
102
- });
103
-
104
- test("no env value → falls to keyDef.default (bridge is conditional, not always-on)", async () => {
105
- const value = await peekPageSize(resolverFor({}));
106
- expect(value).toBe(10);
107
- });
108
-
109
- test("a caller-supplied configResolver overrides the default", () => {
110
- const custom = createConfigResolver();
111
- const extra = mergeConfigResolverDefault({ configResolver: custom }, stack.registry, {
112
- DEVCFG_PAGE_SIZE: "25",
113
- }) as ExtraObj;
114
- expect(extra.configResolver).toBe(custom);
115
- });
116
-
117
- // Regression: the GDPR export download threw errors.internal in prod because
118
- // boot only wired `configResolver`, never `_configAccessorFactory` — so the
119
- // dispatcher left ctx.config undefined and createFileProviderForTenant threw
120
- // "ctx.config is missing". The boot now mints the factory.
121
- test("boot wiring mints _configAccessorFactory so handlers get ctx.config", () => {
122
- const extra = mergeConfigResolverDefault(undefined, stack.registry, {}) as ExtraObj & {
123
- readonly _configAccessorFactory?: unknown;
124
- };
125
- expect(typeof extra._configAccessorFactory).toBe("function");
126
- });
127
-
128
- test("_configAccessorFactory uses the EFFECTIVE resolver (caller override wins over env-bridge)", async () => {
129
- // money-horse pins `file-foundation:config:provider` = "s3-env" via an
130
- // appOverride on its own configResolver; ctx.config MUST read that override,
131
- // not the boot default — else the download resolves no provider.
132
- const override = createConfigResolver({ appOverrides: new Map([[PAGE_SIZE, "99"]]) });
133
- const extra = mergeConfigResolverDefault({ configResolver: override }, stack.registry, {
134
- DEVCFG_PAGE_SIZE: "25", // env-bridge default would be 25 — the override must win
135
- }) as ExtraObj & {
136
- readonly _configAccessorFactory: (deps: {
137
- readonly user: { readonly id: string; readonly tenantId: string };
138
- readonly db: typeof stack.db;
139
- }) => (key: string) => Promise<unknown>;
140
- };
141
- const accessor = extra._configAccessorFactory({
142
- user: { id: TestUsers.systemAdmin.id, tenantId: TestUsers.systemAdmin.tenantId },
143
- db: stack.db,
144
- });
145
- // appOverride values come through raw (string) — what matters is it's the
146
- // override's "99", NOT the env-bridge default ("25"): proves the factory
147
- // was built from the caller's resolver, exactly like money-horse's "s3-env".
148
- expect(await accessor(PAGE_SIZE)).toBe("99");
149
- });
150
-
151
- test("a real dispatch through stack.http resolves ctx.config end-to-end (not the factory called directly)", async () => {
152
- // Every test above calls mergeConfigResolverDefault()/_configAccessorFactory
153
- // directly — none go through an actual dispatched request. The original bug
154
- // this feature guards against ("boot writes configResolver, dispatcher reads
155
- // a different field") only manifests on a REAL dispatch, where the
156
- // dispatcher's own wiring (not test code) builds ctx.config from
157
- // _configAccessorFactory. This is that missing end-to-end proof.
158
- const res = await stack.http.queryOk<{ pageSize: number | undefined }>(
159
- "devcfg:query:read-page-size",
160
- {},
161
- TestUsers.systemAdmin,
162
- );
163
- expect(res.pageSize).toBe(10); // keyDef.default — no env override wired on this stack
164
- });
165
- });
@@ -1,89 +0,0 @@
1
- // Unit-Tests für createCrashTracker — die rollende-Fenster-Logik aus
2
- // kumiko-dev. Hier steckt der Crash-Loop-Schutz, der entscheidet ob der
3
- // Wrapper aufgibt oder noch eine Runde respawnt; off-by-one am Fenster-
4
- // Rand würde im Bin-Skript schlecht auffallen.
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import { createCrashTracker } from "../crash-tracker";
8
-
9
- describe("createCrashTracker", () => {
10
- test("erste maxCrashes Crashes sind erlaubt, der nächste nicht", () => {
11
- const t = createCrashTracker({ maxCrashes: 3, windowMs: 10_000 });
12
- expect(t.noteCrash(1000)).toBe(true);
13
- expect(t.noteCrash(1100)).toBe(true);
14
- expect(t.noteCrash(1200)).toBe(true);
15
- // 4. Crash innerhalb des Fensters → über Limit
16
- expect(t.noteCrash(1300)).toBe(false);
17
- });
18
-
19
- test("alte Crashes außerhalb des Fensters werden geprunt", () => {
20
- const t = createCrashTracker({ maxCrashes: 2, windowMs: 1000 });
21
- t.noteCrash(0);
22
- t.noteCrash(500);
23
- // bei t=1500 ist t=0 raus (1500 - 1000 = 500, alles < 500 fliegt),
24
- // im Fenster bleibt nur t=500. Mit dem neuen Crash bei 1500 sind
25
- // wir bei 2 → noch im Limit.
26
- expect(t.noteCrash(1500)).toBe(true);
27
- expect(t.crashCountInWindow(1500)).toBe(2);
28
- });
29
-
30
- test("crashCountInWindow zählt nur, was im Fenster liegt", () => {
31
- const t = createCrashTracker({ maxCrashes: 5, windowMs: 1000 });
32
- t.noteCrash(0);
33
- t.noteCrash(0);
34
- t.noteCrash(2000); // pruned beide alten weg
35
- expect(t.crashCountInWindow(2000)).toBe(1);
36
- });
37
-
38
- test("crashCountInWindow prunt lazy auch ohne vorheriges noteCrash", () => {
39
- // Wichtig: crashCountInWindow muss eigenständig korrekt sein, nicht
40
- // nur als Folge eines vorangegangenen noteCrash. Sonst lügt der Name.
41
- const t = createCrashTracker({ maxCrashes: 5, windowMs: 1000 });
42
- t.noteCrash(0);
43
- t.noteCrash(100);
44
- // Direkter Aufruf bei now=2000 — alle alten Crashes sind raus.
45
- expect(t.crashCountInWindow(2000)).toBe(0);
46
- // Idempotent: zweiter Aufruf liefert dasselbe.
47
- expect(t.crashCountInWindow(2000)).toBe(0);
48
- });
49
-
50
- test("Boundary: Crash genau am Fenster-Endpoint bleibt im Fenster", () => {
51
- // Endpoint inklusive: cutoff = now - windowMs. Crash bei t=cutoff
52
- // soll noch zählen. Hier: now=1000, windowMs=1000 → cutoff=0.
53
- // Ein vorheriger Crash bei t=0 darf nicht als "alt" geprunt werden.
54
- const t = createCrashTracker({ maxCrashes: 2, windowMs: 1000 });
55
- t.noteCrash(0);
56
- expect(t.noteCrash(1000)).toBe(true);
57
- expect(t.crashCountInWindow(1000)).toBe(2);
58
- // Dritter Crash bei 1000 → 3 im Fenster, über Limit
59
- expect(t.noteCrash(1000)).toBe(false);
60
- });
61
-
62
- test("Mehrere Crashes am gleichen Timestamp zählen einzeln", () => {
63
- const t = createCrashTracker({ maxCrashes: 2, windowMs: 1000 });
64
- expect(t.noteCrash(500)).toBe(true);
65
- expect(t.noteCrash(500)).toBe(true);
66
- expect(t.noteCrash(500)).toBe(false);
67
- });
68
-
69
- test("Nach Wartezeit > windowMs ist das Limit zurückgesetzt", () => {
70
- const t = createCrashTracker({ maxCrashes: 2, windowMs: 1000 });
71
- t.noteCrash(0);
72
- t.noteCrash(100);
73
- expect(t.noteCrash(200)).toBe(false); // über Limit
74
- // Wartezeit, alte Crashes raus
75
- expect(t.noteCrash(2000)).toBe(true);
76
- expect(t.crashCountInWindow(2000)).toBe(1);
77
- });
78
-
79
- test("maxCrashes=1 erlaubt genau einen Crash pro Fenster", () => {
80
- const t = createCrashTracker({ maxCrashes: 1, windowMs: 1000 });
81
- expect(t.noteCrash(0)).toBe(true);
82
- expect(t.noteCrash(500)).toBe(false);
83
- // Bei t=1600 sind beide vorigen Crashes (0 und 500) außerhalb
84
- // (cutoff = 600, 0 < 600 und 500 < 600), Tracker ist leer
85
- // bevor der neue gepusht wird.
86
- expect(t.noteCrash(1600)).toBe(true);
87
- expect(t.crashCountInWindow(1600)).toBe(1);
88
- });
89
- });
@@ -1,54 +0,0 @@
1
- // createKumikoServer error / graceful-degradation paths — boot rejects,
2
- // stylesheet pipeline failures, missing CSS route.
3
-
4
- import { describe, expect, test } from "bun:test";
5
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
6
- import { tmpdir } from "node:os";
7
- import { join } from "node:path";
8
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
9
- import { createKumikoServer } from "../create-kumiko-server";
10
-
11
- const emptyFeature = defineFeature("dev-server-errors-probe", () => {});
12
-
13
- describe("createKumikoServer — client bundle failure", () => {
14
- test("broken clientEntry rejects at boot with client bundle failed", async () => {
15
- const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-bundle-fail-"));
16
- const entry = join(tmpDir, "client.tsx");
17
- writeFileSync(entry, "const x = {{{\n");
18
- try {
19
- await expect(
20
- createKumikoServer({
21
- features: [emptyFeature],
22
- port: 0,
23
- installSignalHandlers: false,
24
- clientEntry: entry,
25
- stylesheet: false,
26
- }),
27
- ).rejects.toThrow(/client bundle failed|Bundle failed/);
28
- } finally {
29
- rmSync(tmpDir, { recursive: true, force: true });
30
- }
31
- });
32
-
33
- test("_buildBundle throw propagates at boot", async () => {
34
- const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-stub-fail-"));
35
- const entry = join(tmpDir, "client.tsx");
36
- writeFileSync(entry, "// noop\n");
37
- try {
38
- await expect(
39
- createKumikoServer({
40
- features: [emptyFeature],
41
- port: 0,
42
- installSignalHandlers: false,
43
- clientEntry: entry,
44
- stylesheet: false,
45
- _buildBundle: async () => {
46
- throw new Error("stub build blew up");
47
- },
48
- }),
49
- ).rejects.toThrow(/stub build blew up/);
50
- } finally {
51
- rmSync(tmpDir, { recursive: true, force: true });
52
- }
53
- });
54
- });
@@ -1,33 +0,0 @@
1
- // Option/error matrix for createKumikoServer — pins boot-time rejects
2
- // that happen before Postgres/Redis wiring (normalizeEntries).
3
-
4
- import { describe, expect, test } from "bun:test";
5
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
6
- import { createKumikoServer } from "../create-kumiko-server";
7
-
8
- const emptyFeature = defineFeature("prod-packaging-options-probe", () => {});
9
-
10
- describe("createKumikoServer — option matrix", () => {
11
- test("clientEntry + clientEntries → mutually exclusive error", async () => {
12
- await expect(
13
- createKumikoServer({
14
- features: [emptyFeature],
15
- clientEntry: "src/client.tsx",
16
- clientEntries: [{ name: "admin", sourceFile: "src/client-admin.tsx" }],
17
- installSignalHandlers: false,
18
- port: 0,
19
- }),
20
- ).rejects.toThrow(/mutually exclusive/);
21
- });
22
-
23
- test("clientEntries without hostDispatch → error", async () => {
24
- await expect(
25
- createKumikoServer({
26
- features: [emptyFeature],
27
- clientEntries: [{ name: "admin", sourceFile: "src/client-admin.tsx" }],
28
- installSignalHandlers: false,
29
- port: 0,
30
- }),
31
- ).rejects.toThrow(/hostDispatch/);
32
- });
33
- });