@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.
- package/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- 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
|
-
});
|