@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,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { formatBuildResult } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
|
|
6
|
-
import { discoverServerEntry } from "../build-server-bundle";
|
|
7
|
-
|
|
8
|
-
describe("discoverServerEntry", () => {
|
|
9
|
-
test("finds bin/main.ts when present", () => {
|
|
10
|
-
const dir = mkdtempSync(join(tmpdir(), "kumiko-discover-"));
|
|
11
|
-
mkdirSync(join(dir, "bin"));
|
|
12
|
-
writeFileSync(join(dir, "bin/main.ts"), "export {};\n", "utf8");
|
|
13
|
-
expect(discoverServerEntry(dir)).toBe(join(dir, "bin/main.ts"));
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("returns undefined when no entry exists", () => {
|
|
17
|
-
const dir = mkdtempSync(join(tmpdir(), "kumiko-discover-empty-"));
|
|
18
|
-
expect(discoverServerEntry(dir)).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("formatBuildResult", () => {
|
|
23
|
-
test("includes outDir and manifest entries", () => {
|
|
24
|
-
const out = formatBuildResult(
|
|
25
|
-
{ outDir: "dist/client", manifest: { "app.js": "app.abc123.js" } },
|
|
26
|
-
42,
|
|
27
|
-
);
|
|
28
|
-
expect(out).toContain("dist/client");
|
|
29
|
-
expect(out).toContain("app.js");
|
|
30
|
-
expect(out).toContain("42ms");
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
// Integration-style spec for runProdApp's envSchema boot-stage.
|
|
2
|
-
// Focus: the env-validation path BEFORE any DB/Redis connection.
|
|
3
|
-
// We never reach `requireEnv("DATABASE_URL")` here — invalid env
|
|
4
|
-
// throws (via bootErrorReporter override) and KUMIKO_DRY_RUN_ENV
|
|
5
|
-
// returns the dry-run handle without booting.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from "bun:test";
|
|
8
|
-
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
-
import { composeEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
|
|
10
|
-
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
11
|
-
import * as z from "zod";
|
|
12
|
-
import { frameworkCoreEnvSchema } from "../env-schema";
|
|
13
|
-
import * as devServerPublicApi from "../index";
|
|
14
|
-
|
|
15
|
-
const secretsFeature = defineFeature("secrets", (r) => {
|
|
16
|
-
r.envSchema(
|
|
17
|
-
z.object({
|
|
18
|
-
KUMIKO_SECRETS_MASTER_KEY_V1: z
|
|
19
|
-
.string()
|
|
20
|
-
.describe("AES-256 KEK")
|
|
21
|
-
.meta({ kumiko: { pulumi: { generator: "openssl rand -base64 32", secret: true } } }),
|
|
22
|
-
}),
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const authFeature = defineFeature("auth-email-password", (r) => {
|
|
27
|
-
r.envSchema(
|
|
28
|
-
z.object({
|
|
29
|
-
JWT_SECRET: z.string().min(32).describe("JWT signing key"),
|
|
30
|
-
}),
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const composed = composeEnvSchema({
|
|
35
|
-
features: [secretsFeature, authFeature],
|
|
36
|
-
extend: z.object({
|
|
37
|
-
STUDIO_ADMIN_EMAIL: z.email(),
|
|
38
|
-
}),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("public-export smoke", () => {
|
|
42
|
-
it("re-exports frameworkCoreEnvSchema via dev-server's package entry", () => {
|
|
43
|
-
expect(devServerPublicApi.frameworkCoreEnvSchema).toBe(frameworkCoreEnvSchema);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("re-exports frameworkCoreEnvSchema via the env-schema subpath", async () => {
|
|
47
|
-
const viaSubpath = await import("@cosmicdrift/kumiko-dev-server/env-schema");
|
|
48
|
-
expect(viaSubpath.frameworkCoreEnvSchema).toBe(frameworkCoreEnvSchema);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("runProdApp envSchema integration", () => {
|
|
53
|
-
it("aggregates all env-errors at boot, not first-fail", async () => {
|
|
54
|
-
let captured: KumikoBootError | undefined;
|
|
55
|
-
try {
|
|
56
|
-
await runProdApp({
|
|
57
|
-
features: [],
|
|
58
|
-
envSchema: composed,
|
|
59
|
-
pulumiPrefix: "studio",
|
|
60
|
-
envSource: {
|
|
61
|
-
// KUMIKO_SECRETS_MASTER_KEY_V1 missing
|
|
62
|
-
JWT_SECRET: "short", // invalid (min 32)
|
|
63
|
-
STUDIO_ADMIN_EMAIL: "not-an-email", // invalid format
|
|
64
|
-
},
|
|
65
|
-
bootErrorReporter: (err) => {
|
|
66
|
-
captured = err;
|
|
67
|
-
throw err;
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
} catch (err) {
|
|
71
|
-
expect(err).toBeInstanceOf(KumikoBootError);
|
|
72
|
-
}
|
|
73
|
-
expect(captured).toBeDefined();
|
|
74
|
-
expect(captured!.errors.length).toBe(3);
|
|
75
|
-
const names = captured!.errors.map((e) => e.name).sort();
|
|
76
|
-
expect(names).toEqual(["JWT_SECRET", "KUMIKO_SECRETS_MASTER_KEY_V1", "STUDIO_ADMIN_EMAIL"]);
|
|
77
|
-
const kek = captured!.errors.find((e) => e.name === "KUMIKO_SECRETS_MASTER_KEY_V1");
|
|
78
|
-
expect(kek?.kind).toBe("missing");
|
|
79
|
-
expect(kek?.suggestion).toBe(
|
|
80
|
-
'Set via: pulumi config set --secret studioKumikoSecretsMasterKeyV1 "$(openssl rand -base64 32)"',
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("KUMIKO_DRY_RUN_ENV=human prints inventory and returns a dry-run handle", async () => {
|
|
85
|
-
const logs: string[] = [];
|
|
86
|
-
const realLog = console.log;
|
|
87
|
-
console.log = (...args: unknown[]) => {
|
|
88
|
-
logs.push(args.map(String).join(" "));
|
|
89
|
-
};
|
|
90
|
-
try {
|
|
91
|
-
const handle = await runProdApp({
|
|
92
|
-
features: [],
|
|
93
|
-
envSchema: composed,
|
|
94
|
-
envSource: {
|
|
95
|
-
KUMIKO_DRY_RUN_ENV: "human",
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
expect(handle).toBeDefined();
|
|
99
|
-
const out = logs.join("\n");
|
|
100
|
-
expect(out).toContain("Required env-vars:");
|
|
101
|
-
expect(out).toContain("KUMIKO_SECRETS_MASTER_KEY_V1");
|
|
102
|
-
expect(out).toContain("(secrets)");
|
|
103
|
-
expect(out).toContain("JWT_SECRET");
|
|
104
|
-
expect(out).toContain("(auth-email-password)");
|
|
105
|
-
expect(out).toContain("STUDIO_ADMIN_EMAIL");
|
|
106
|
-
expect(out).toContain("(app)");
|
|
107
|
-
} finally {
|
|
108
|
-
console.log = realLog;
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("KUMIKO_DRY_RUN_ENV=pulumi emits `pulumi config set` lines with prefix", async () => {
|
|
113
|
-
const logs: string[] = [];
|
|
114
|
-
const realLog = console.log;
|
|
115
|
-
console.log = (...args: unknown[]) => {
|
|
116
|
-
logs.push(args.map(String).join(" "));
|
|
117
|
-
};
|
|
118
|
-
try {
|
|
119
|
-
await runProdApp({
|
|
120
|
-
features: [],
|
|
121
|
-
envSchema: composed,
|
|
122
|
-
pulumiPrefix: "studio",
|
|
123
|
-
envSource: { KUMIKO_DRY_RUN_ENV: "pulumi" },
|
|
124
|
-
});
|
|
125
|
-
const out = logs.join("\n");
|
|
126
|
-
expect(out).toContain(
|
|
127
|
-
'pulumi config set --secret studioKumikoSecretsMasterKeyV1 "$(openssl rand -base64 32)"',
|
|
128
|
-
);
|
|
129
|
-
expect(out).toContain('pulumi config set studioStudioAdminEmail "<set-me>"');
|
|
130
|
-
} finally {
|
|
131
|
-
console.log = realLog;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("composes frameworkCoreEnvSchema with feature schemas; reports core-source on missing PORT/DATABASE_URL", async () => {
|
|
136
|
-
const composedWithCore = composeEnvSchema({
|
|
137
|
-
core: frameworkCoreEnvSchema,
|
|
138
|
-
features: [secretsFeature, authFeature],
|
|
139
|
-
extend: z.object({ STUDIO_ADMIN_EMAIL: z.email() }),
|
|
140
|
-
});
|
|
141
|
-
let captured: KumikoBootError | undefined;
|
|
142
|
-
try {
|
|
143
|
-
await runProdApp({
|
|
144
|
-
features: [],
|
|
145
|
-
envSchema: composedWithCore,
|
|
146
|
-
pulumiPrefix: "studio",
|
|
147
|
-
envSource: {
|
|
148
|
-
// DATABASE_URL + REDIS_URL missing (framework-core)
|
|
149
|
-
// JWT_SECRET + KUMIKO_SECRETS_MASTER_KEY_V1 missing (features)
|
|
150
|
-
// STUDIO_ADMIN_EMAIL missing (app)
|
|
151
|
-
PORT: "invalid-port",
|
|
152
|
-
},
|
|
153
|
-
bootErrorReporter: (err) => {
|
|
154
|
-
captured = err;
|
|
155
|
-
throw err;
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
} catch (err) {
|
|
159
|
-
expect(err).toBeInstanceOf(KumikoBootError);
|
|
160
|
-
}
|
|
161
|
-
expect(captured).toBeDefined();
|
|
162
|
-
const port = captured!.errors.find((e) => e.name === "PORT");
|
|
163
|
-
expect(port?.source).toBe("framework-core");
|
|
164
|
-
expect(port?.kind).toBe("invalid");
|
|
165
|
-
const db = captured!.errors.find((e) => e.name === "DATABASE_URL");
|
|
166
|
-
expect(db?.source).toBe("framework-core");
|
|
167
|
-
expect(db?.kind).toBe("missing");
|
|
168
|
-
const formatted = captured!.format();
|
|
169
|
-
expect(formatted).toContain("✗ DATABASE_URL (framework-core, required, missing)");
|
|
170
|
-
expect(formatted).toContain("✗ REDIS_URL (framework-core, required, missing)");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("KUMIKO_DRY_RUN_ENV=1 aliases to human-mode", async () => {
|
|
174
|
-
const logs: string[] = [];
|
|
175
|
-
const realLog = console.log;
|
|
176
|
-
console.log = (...args: unknown[]) => {
|
|
177
|
-
logs.push(args.map(String).join(" "));
|
|
178
|
-
};
|
|
179
|
-
try {
|
|
180
|
-
await runProdApp({
|
|
181
|
-
features: [],
|
|
182
|
-
envSchema: composed,
|
|
183
|
-
envSource: { KUMIKO_DRY_RUN_ENV: "1" },
|
|
184
|
-
});
|
|
185
|
-
expect(logs.join("\n")).toContain("Required env-vars:");
|
|
186
|
-
} finally {
|
|
187
|
-
console.log = realLog;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("KUMIKO_DRY_RUN_ENV=boot runs validators + exits before DB-connect", async () => {
|
|
192
|
-
const logs: string[] = [];
|
|
193
|
-
const realLog = console.log;
|
|
194
|
-
console.log = (...args: unknown[]) => {
|
|
195
|
-
logs.push(args.map(String).join(" "));
|
|
196
|
-
};
|
|
197
|
-
try {
|
|
198
|
-
const handle = await runProdApp({
|
|
199
|
-
features: [secretsFeature, authFeature],
|
|
200
|
-
envSchema: composed,
|
|
201
|
-
envSource: {
|
|
202
|
-
KUMIKO_DRY_RUN_ENV: "boot",
|
|
203
|
-
KUMIKO_SECRETS_MASTER_KEY_V1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
204
|
-
JWT_SECRET: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
205
|
-
STUDIO_ADMIN_EMAIL: "ops@example.com",
|
|
206
|
-
DATABASE_URL: "postgres://dummy:dummy@127.0.0.1:1/dummy",
|
|
207
|
-
REDIS_URL: "redis://127.0.0.1:1",
|
|
208
|
-
},
|
|
209
|
-
migrations: false,
|
|
210
|
-
});
|
|
211
|
-
expect(logs.join("\n")).toContain("boot validation OK");
|
|
212
|
-
expect(handle).toBeDefined();
|
|
213
|
-
} finally {
|
|
214
|
-
console.log = realLog;
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("KUMIKO_DRY_RUN_ENV=boot does not skip env-validation (no success handle on invalid env)", async () => {
|
|
219
|
-
// Env-error aggregation itself is mode-independent (covered above). The
|
|
220
|
-
// boot-specific guarantee here: the boot dry-run — which returns a handle
|
|
221
|
-
// and exits early on VALID env (see the test above) — must NOT report
|
|
222
|
-
// success on INVALID env. It aborts with the aggregated errors and returns
|
|
223
|
-
// no handle, so a regression that made boot-mode bail out before validation
|
|
224
|
-
// would surface as a falsely-returned handle.
|
|
225
|
-
let captured: KumikoBootError | undefined;
|
|
226
|
-
let handle: unknown;
|
|
227
|
-
try {
|
|
228
|
-
handle = await runProdApp({
|
|
229
|
-
features: [secretsFeature, authFeature],
|
|
230
|
-
envSchema: composed,
|
|
231
|
-
envSource: {
|
|
232
|
-
KUMIKO_DRY_RUN_ENV: "boot",
|
|
233
|
-
JWT_SECRET: "short",
|
|
234
|
-
STUDIO_ADMIN_EMAIL: "not-an-email",
|
|
235
|
-
},
|
|
236
|
-
bootErrorReporter: (err) => {
|
|
237
|
-
captured = err;
|
|
238
|
-
throw err;
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
} catch (err) {
|
|
242
|
-
expect(err).toBeInstanceOf(KumikoBootError);
|
|
243
|
-
}
|
|
244
|
-
expect(handle).toBeUndefined();
|
|
245
|
-
expect(captured).toBeDefined();
|
|
246
|
-
expect(captured!.errors.length).toBeGreaterThanOrEqual(3);
|
|
247
|
-
});
|
|
248
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { composeEnvSchema, KumikoBootError, parseEnv } from "@cosmicdrift/kumiko-framework/env";
|
|
3
|
-
import * as z from "zod";
|
|
4
|
-
import { type FrameworkCoreEnv, frameworkCoreEnvSchema } from "../env-schema";
|
|
5
|
-
|
|
6
|
-
// Statt Sentinel-throw IM try (ein nicht-werfender parseEnv ließe den
|
|
7
|
-
// Sentinel in den catch fallen und produziert eine irreführende
|
|
8
|
-
// instanceOf-Failure): laufen lassen, danach unconditional asserten.
|
|
9
|
-
function expectBootError(run: () => void): KumikoBootError {
|
|
10
|
-
let thrown: unknown;
|
|
11
|
-
try {
|
|
12
|
-
run();
|
|
13
|
-
} catch (err) {
|
|
14
|
-
thrown = err;
|
|
15
|
-
}
|
|
16
|
-
expect(thrown).toBeInstanceOf(KumikoBootError);
|
|
17
|
-
return thrown as KumikoBootError;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("frameworkCoreEnvSchema", () => {
|
|
21
|
-
it("accepts a valid minimal env", () => {
|
|
22
|
-
const env = parseEnv(frameworkCoreEnvSchema, {
|
|
23
|
-
DATABASE_URL: "postgres://localhost:5432/db",
|
|
24
|
-
REDIS_URL: "redis://localhost:6379",
|
|
25
|
-
});
|
|
26
|
-
// Defaults populated, optional keys remain undefined.
|
|
27
|
-
expect(env.PORT).toBe("3000");
|
|
28
|
-
expect(env.DATABASE_URL).toBe("postgres://localhost:5432/db");
|
|
29
|
-
expect(env.REDIS_URL).toBe("redis://localhost:6379");
|
|
30
|
-
expect(env.KUMIKO_INSTANCE_ID).toBeUndefined();
|
|
31
|
-
expect(env.KUMIKO_SKIP_ES_OPS).toBeUndefined();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("aggregates missing required vars (DATABASE_URL + REDIS_URL)", () => {
|
|
35
|
-
const boot = expectBootError(() => parseEnv(frameworkCoreEnvSchema, {}));
|
|
36
|
-
const names = boot.errors.map((e) => e.name).sort();
|
|
37
|
-
expect(names).toContain("DATABASE_URL");
|
|
38
|
-
expect(names).toContain("REDIS_URL");
|
|
39
|
-
// PORT has a default → not in the error set
|
|
40
|
-
expect(names).not.toContain("PORT");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("rejects a non-postgres DATABASE_URL even when it is a valid WHATWG URL", () => {
|
|
44
|
-
const boot = expectBootError(() =>
|
|
45
|
-
parseEnv(frameworkCoreEnvSchema, {
|
|
46
|
-
DATABASE_URL: "https://example.com/db",
|
|
47
|
-
REDIS_URL: "redis://localhost:6379",
|
|
48
|
-
}),
|
|
49
|
-
);
|
|
50
|
-
const db = boot.errors.find((e) => e.name === "DATABASE_URL");
|
|
51
|
-
expect(db?.kind).toBe("invalid");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("accepts postgres:// and postgresql:// for DATABASE_URL", () => {
|
|
55
|
-
for (const url of ["postgres://localhost:5432/db", "postgresql://localhost:5432/db"]) {
|
|
56
|
-
const env = parseEnv(frameworkCoreEnvSchema, {
|
|
57
|
-
DATABASE_URL: url,
|
|
58
|
-
REDIS_URL: "redis://localhost:6379",
|
|
59
|
-
});
|
|
60
|
-
expect(env.DATABASE_URL).toBe(url);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("rejects a non-redis REDIS_URL and accepts redis:// + rediss://", () => {
|
|
65
|
-
const boot = expectBootError(() =>
|
|
66
|
-
parseEnv(frameworkCoreEnvSchema, {
|
|
67
|
-
DATABASE_URL: "postgres://localhost:5432/db",
|
|
68
|
-
REDIS_URL: "https://example.com/cache",
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
71
|
-
const redis = boot.errors.find((e) => e.name === "REDIS_URL");
|
|
72
|
-
expect(redis?.kind).toBe("invalid");
|
|
73
|
-
for (const url of ["redis://localhost:6379", "rediss://localhost:6379"]) {
|
|
74
|
-
const env = parseEnv(frameworkCoreEnvSchema, {
|
|
75
|
-
DATABASE_URL: "postgres://localhost:5432/db",
|
|
76
|
-
REDIS_URL: url,
|
|
77
|
-
});
|
|
78
|
-
expect(env.REDIS_URL).toBe(url);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("rejects an invalid PORT (non-numeric)", () => {
|
|
83
|
-
const boot = expectBootError(() =>
|
|
84
|
-
parseEnv(frameworkCoreEnvSchema, {
|
|
85
|
-
DATABASE_URL: "postgres://localhost:5432/db",
|
|
86
|
-
REDIS_URL: "redis://localhost:6379",
|
|
87
|
-
PORT: "not-a-port",
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
const port = boot.errors.find((e) => e.name === "PORT");
|
|
91
|
-
expect(port?.kind).toBe("invalid");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("KUMIKO_SKIP_ES_OPS accepts any string (matches runtime semantics)", () => {
|
|
95
|
-
// Runtime check is `!== "1"` — non-"1" values are silently ignored.
|
|
96
|
-
// The schema mirrors that: validate string-or-unset, not "1"-only.
|
|
97
|
-
const env = parseEnv(frameworkCoreEnvSchema, {
|
|
98
|
-
DATABASE_URL: "postgres://localhost:5432/db",
|
|
99
|
-
REDIS_URL: "redis://localhost:6379",
|
|
100
|
-
KUMIKO_SKIP_ES_OPS: "true",
|
|
101
|
-
});
|
|
102
|
-
expect(env.KUMIKO_SKIP_ES_OPS).toBe("true");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("composes into an app-wide schema with source attribution", () => {
|
|
106
|
-
const { schema, sources } = composeEnvSchema({
|
|
107
|
-
core: frameworkCoreEnvSchema,
|
|
108
|
-
features: [],
|
|
109
|
-
extend: z.object({ STUDIO_ADMIN_EMAIL: z.email() }),
|
|
110
|
-
});
|
|
111
|
-
expect(sources["DATABASE_URL"]).toBe("framework-core");
|
|
112
|
-
expect(sources["PORT"]).toBe("framework-core");
|
|
113
|
-
expect(sources["STUDIO_ADMIN_EMAIL"]).toBe("app");
|
|
114
|
-
const boot = expectBootError(() => parseEnv(schema, {}, { sources }));
|
|
115
|
-
const formatted = boot.format();
|
|
116
|
-
expect(formatted).toContain("✗ DATABASE_URL (framework-core, required, missing)");
|
|
117
|
-
expect(formatted).toContain("✗ REDIS_URL (framework-core, required, missing)");
|
|
118
|
-
expect(formatted).toContain("✗ STUDIO_ADMIN_EMAIL (app, required, missing)");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("z.infer<typeof frameworkCoreEnvSchema> typechecks the expected shape", () => {
|
|
122
|
-
// Compile-time test — typecheck must accept this shape exactly.
|
|
123
|
-
const env: FrameworkCoreEnv = {
|
|
124
|
-
PORT: "3000",
|
|
125
|
-
DATABASE_URL: "postgres://localhost/x",
|
|
126
|
-
REDIS_URL: "redis://localhost",
|
|
127
|
-
KUMIKO_INSTANCE_ID: "pod-7",
|
|
128
|
-
KUMIKO_SKIP_ES_OPS: "1",
|
|
129
|
-
};
|
|
130
|
-
expect(env.PORT).toBe("3000");
|
|
131
|
-
});
|
|
132
|
-
});
|