@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.2

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,464 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { checkDeployDrift, render, scaffoldDeploy } from "../scaffold-deploy";
6
-
7
- describe("scaffoldDeploy", () => {
8
- let tmp: string;
9
-
10
- beforeEach(() => {
11
- tmp = mkdtempSync(join(tmpdir(), "kumiko-deploy-"));
12
- });
13
- afterEach(() => {
14
- rmSync(tmp, { recursive: true, force: true });
15
- });
16
-
17
- it("generates Dockerfile, Dockerfile.dockerignore, migrate-step.sh", () => {
18
- const result = scaffoldDeploy({ appName: "myapp", destination: tmp });
19
- expect(result.destination).toBe(join(tmp, "deploy"));
20
- expect(result.files).toHaveLength(3);
21
- expect(result.files.every((f) => f.written)).toBe(true);
22
- expect(existsSync(join(tmp, "deploy", "Dockerfile"))).toBe(true);
23
- expect(existsSync(join(tmp, "deploy", "Dockerfile.dockerignore"))).toBe(true);
24
- expect(existsSync(join(tmp, "deploy", "migrate-step.sh"))).toBe(true);
25
- });
26
-
27
- it("substitutes {{appName}} + {{port}} + {{githubOrg}}", () => {
28
- scaffoldDeploy({
29
- appName: "myapp",
30
- port: 4242,
31
- githubOrg: "acme",
32
- destination: tmp,
33
- });
34
- const dockerfile = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
35
- expect(dockerfile).toContain("Production-Image for myapp");
36
- expect(dockerfile).toContain("ENV PORT=4242");
37
- expect(dockerfile).toContain("EXPOSE 4242");
38
-
39
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
40
- expect(migrate).toContain("myapp pre-deploy migrate step");
41
- expect(migrate).toContain("ghcr.io/acme/myapp:latest");
42
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
43
- expect(migrate).toContain("postgresql://myapp:${DB_PASSWORD}@db:5432/myapp");
44
- // The password-bearing URL is composed into the shell env and passed by
45
- // NAME (`-e DATABASE_URL`, no `=`), so the expanded value never lands in
46
- // `docker run`'s argv (would be visible in `ps auxe`).
47
- expect(migrate).toContain("export DATABASE_URL=");
48
- expect(migrate).toMatch(/-e DATABASE_URL\b(?!=)/);
49
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
50
- expect(migrate).not.toContain('-e DATABASE_URL="postgresql://myapp:${DB_PASSWORD}');
51
- // grep's no-match exit must not abort the script before the friendly
52
- // "No _stack network found" branch runs.
53
- expect(migrate).toContain("| head -1 || true)");
54
- // Docker template syntax {{.Name}} must pass through verbatim — our
55
- // placeholder regex only matches lowercase-leading identifiers.
56
- expect(migrate).toContain('"{{.Name}}"');
57
- });
58
-
59
- it("uses defaults when port + githubOrg are omitted", () => {
60
- scaffoldDeploy({ appName: "minimal", destination: tmp });
61
- const dockerfile = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
62
- expect(dockerfile).toContain("ENV PORT=3000");
63
- expect(dockerfile).toContain("EXPOSE 3000");
64
-
65
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
66
- expect(migrate).toContain("ghcr.io/cosmicdriftgamestudio/minimal:latest");
67
- });
68
-
69
- it("Dockerfile emits inline start.sh (createBunServer command-override target)", () => {
70
- scaffoldDeploy({ appName: "boot-target", destination: tmp });
71
- const dockerfile = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
72
- // Inline RUN that creates a start.sh inside the runtime image.
73
- // bun-server.ts's createBunServer overrides the container command with
74
- // `exec ./start.sh` after injecting DATABASE_URL; without this line the
75
- // pod exited 127. Memory: `feedback_audit_drift_root_cause_now`.
76
- expect(dockerfile).toContain("> ./start.sh && chmod +x ./start.sh");
77
- expect(dockerfile).toContain("exec bun run server.js");
78
- });
79
-
80
- it("Dockerfile copies kumiko/migrations and is free of stale drizzle artefacts", () => {
81
- // Regression guard for PR #167 deploy-template drift: after the drizzle
82
- // replacement (framework 0.21) + single-bundle server build (0.20), the
83
- // template kept three dead drizzle references that broke fresh deploys —
84
- // a COPY for the unbundled drizzle.config.ts, a COPY of /app/drizzle
85
- // (apps now ship kumiko/), and a KUMIKO_MIGRATION_HOOKS env pointing at
86
- // a no-longer-bundled migration-hooks.js. The migrate path is now
87
- // `kumiko schema apply` reading ${INIT_CWD}/kumiko/migrations/*.sql.
88
- scaffoldDeploy({ appName: "drift-guard", destination: tmp });
89
- const dockerfile = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
90
-
91
- expect(dockerfile).toContain(
92
- "COPY --from=build --chown=app:app /app/kumiko/migrations ./kumiko/migrations",
93
- );
94
-
95
- expect(dockerfile).not.toContain("/app/dist-server/drizzle.config.ts");
96
- expect(dockerfile).not.toContain("COPY --from=build --chown=app:app /app/drizzle ./drizzle");
97
- expect(dockerfile).not.toContain("KUMIKO_MIGRATION_HOOKS");
98
- expect(dockerfile).not.toContain("migration-hooks.js");
99
- });
100
-
101
- it("migrate-step.sh invokes the schema-CLI subcommand registered in kumiko.ts", () => {
102
- // Regression guard for PR #167: the deploy step was calling
103
- // `bun /app/kumiko.js migrate apply`, but the CLI only registers
104
- // `schema` (with subcommand `apply`). The pre-deploy migrate step
105
- // crashed with "Unknown command: migrate" until this was fixed.
106
- scaffoldDeploy({ appName: "cli-cmd", destination: tmp });
107
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
108
- expect(migrate).toContain("bun /app/kumiko.js schema apply");
109
- expect(migrate).not.toContain("kumiko.js migrate apply");
110
- });
111
-
112
- it("skips existing files by default", () => {
113
- const existing = join(tmp, "deploy");
114
- scaffoldDeploy({ appName: "first", destination: tmp });
115
- writeFileSync(join(existing, "Dockerfile"), "# user-tuned, do not touch");
116
- const second = scaffoldDeploy({ appName: "first", destination: tmp });
117
- const dockerfile = second.files.find((f) => f.path.endsWith("/Dockerfile"));
118
- expect(dockerfile?.written).toBe(false);
119
- expect(dockerfile?.reason).toBe("exists");
120
- expect(readFileSync(join(existing, "Dockerfile"), "utf-8")).toBe("# user-tuned, do not touch");
121
- });
122
-
123
- it("overwrites existing files with --force and tags reason='force'", () => {
124
- scaffoldDeploy({ appName: "first", destination: tmp });
125
- writeFileSync(join(tmp, "deploy", "Dockerfile"), "# old content");
126
- const second = scaffoldDeploy({ appName: "first", destination: tmp, force: true });
127
- const dockerfile = second.files.find((f) => f.path.endsWith("/Dockerfile"));
128
- expect(dockerfile?.written).toBe(true);
129
- expect(dockerfile?.reason).toBe("force");
130
- expect(readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8")).toContain(
131
- "Production-Image for first",
132
- );
133
- });
134
-
135
- it("force=true on a fresh directory leaves reason undefined (no clobber happened)", () => {
136
- // Regression guard: a clean first-time write with force=true must NOT
137
- // be tagged as `reason: "force"` — that label is reserved for actual
138
- // overwrites of pre-existing files. Fixed in self-review after the
139
- // initial implementation set `reason: "force"` unconditionally
140
- // post-write.
141
- const result = scaffoldDeploy({ appName: "fresh", destination: tmp, force: true });
142
- for (const f of result.files) {
143
- expect(f.written).toBe(true);
144
- expect(f.reason).toBeUndefined();
145
- }
146
- });
147
-
148
- it("rejects appName that isn't kebab-case", () => {
149
- expect(() => scaffoldDeploy({ appName: "MyApp", destination: tmp })).toThrow(/kebab-case/);
150
- expect(() => scaffoldDeploy({ appName: "my_app", destination: tmp })).toThrow(/kebab-case/);
151
- expect(() => scaffoldDeploy({ appName: "1app", destination: tmp })).toThrow(/kebab-case/);
152
- });
153
-
154
- it("rejects out-of-range port", () => {
155
- expect(() => scaffoldDeploy({ appName: "x", port: 0, destination: tmp })).toThrow(/1\.\.65535/);
156
- expect(() => scaffoldDeploy({ appName: "x", port: 70000, destination: tmp })).toThrow(
157
- /1\.\.65535/,
158
- );
159
- });
160
-
161
- describe("render placeholder consumption", () => {
162
- it("passes Docker/Go template syntax {{.X}} through verbatim", () => {
163
- const out = render('docker network ls --format "{{.Name}}"', {}, {});
164
- expect(out).toBe('docker network ls --format "{{.Name}}"');
165
- });
166
-
167
- it("substitutes known {{key}} placeholders", () => {
168
- expect(render("hello {{who}}", { who: "world" }, {})).toBe("hello world");
169
- });
170
-
171
- it("throws on an orphan close-tag (no opener) instead of leaking it", () => {
172
- expect(() => render("line\n{{/hasSeeds}}\nmore", {}, {})).toThrow(/unconsumed mustache/);
173
- });
174
-
175
- it("strips a matched block but does not leave its close-tag behind", () => {
176
- const out = render("a\n{{#flag}}\nkept\n{{/flag}}\nb", {}, { flag: true });
177
- expect(out).toContain("kept");
178
- expect(out).not.toContain("{{/flag}}");
179
- });
180
- });
181
-
182
- describe("source-tree detection", () => {
183
- it("emits seeds-COPY block only when seeds/ exists", () => {
184
- // Without seeds/: block stripped
185
- const without = scaffoldDeploy({ appName: "noseeds", destination: tmp });
186
- expect(without.detected.hasSeeds).toBe(false);
187
- const dfNo = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
188
- expect(dfNo).not.toContain("COPY --from=build --chown=app:app /app/seeds ./seeds");
189
- expect(dfNo).not.toContain("ES-Operations seed migrations");
190
- });
191
-
192
- it("emits seeds-COPY block when seeds/ exists", () => {
193
- mkdirSync(join(tmp, "seeds"), { recursive: true });
194
- writeFileSync(join(tmp, "seeds", ".keep"), "");
195
- const result = scaffoldDeploy({ appName: "withseeds", destination: tmp });
196
- expect(result.detected.hasSeeds).toBe(true);
197
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
198
- expect(df).toContain("COPY --from=build --chown=app:app /app/seeds ./seeds");
199
- expect(df).toContain("ES-Operations seed migrations");
200
- });
201
-
202
- it("emits GITHUB_TOKEN blocks when @cosmicdriftgamestudio/* dep is present", () => {
203
- writeFileSync(
204
- join(tmp, "package.json"),
205
- JSON.stringify({
206
- name: "ghapp",
207
- dependencies: {
208
- "@cosmicdriftgamestudio/kumiko-ai-foundation": "^0.2.0",
209
- },
210
- }),
211
- );
212
- const result = scaffoldDeploy({ appName: "ghapp", destination: tmp });
213
- expect(result.detected.hasPrivateGhPackages).toBe(true);
214
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
215
- expect(df).toContain("ARG NPM_AUTH_TOKEN=");
216
- expect(df).toContain("ARG NPM_AUTH_TOKEN\n");
217
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell variable expansion, not a JS template
218
- expect(df).toContain("ENV GITHUB_TOKEN=${NPM_AUTH_TOKEN}");
219
- });
220
-
221
- it("skips NPM_AUTH_TOKEN blocks when only public @cosmicdrift/* deps are present", () => {
222
- writeFileSync(
223
- join(tmp, "package.json"),
224
- JSON.stringify({
225
- name: "publicapp",
226
- dependencies: {
227
- "@cosmicdrift/kumiko-framework": "^0.8.0",
228
- "@cosmicdrift/kumiko-bundled-features": "^0.8.0",
229
- },
230
- }),
231
- );
232
- const result = scaffoldDeploy({ appName: "publicapp", destination: tmp });
233
- expect(result.detected.hasPrivateGhPackages).toBe(false);
234
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
235
- expect(df).not.toContain("ARG NPM_AUTH_TOKEN");
236
- expect(df).not.toContain("ENV GITHUB_TOKEN");
237
- });
238
-
239
- it("manifests app with bunfig.toml: NPM_AUTH_TOKEN wiring + manifests-first COPY", () => {
240
- writeFileSync(
241
- join(tmp, "package.json"),
242
- JSON.stringify({
243
- name: "manifestapp",
244
- dependencies: { "@cosmicdriftgamestudio/kumiko-ai-foundation": "^0.2.0" },
245
- }),
246
- );
247
- writeFileSync(join(tmp, "bunfig.toml"), "[install.scopes]\n");
248
- const result = scaffoldDeploy({ appName: "manifestapp", destination: tmp });
249
- expect(result.detected.installFromFullTree).toBe(false);
250
- expect(result.detected.registryConfigFiles).toEqual(["bunfig.toml"]);
251
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
252
- expect(df).toContain("ARG NPM_AUTH_TOKEN=");
253
- expect(df).toContain("ARG NPM_AUTH_TOKEN\n");
254
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell variable expansion, not a JS template
255
- expect(df).toContain("ENV GITHUB_TOKEN=${NPM_AUTH_TOKEN}");
256
- expect(df).toContain(
257
- "COPY package.json bun.lock bunfig.toml ./\nRUN bun install --frozen-lockfile",
258
- );
259
- expect(df).toMatch(
260
- /FROM .* AS runtime\n(?:#[^\n]*\n)*ARG BUILD_VERSION=dev\nARG BUILD_TIME=unknown/,
261
- );
262
- });
263
-
264
- it(".npmrc-only app copies .npmrc before install", () => {
265
- writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "npmrcapp" }));
266
- writeFileSync(
267
- join(tmp, ".npmrc"),
268
- "@cosmicdriftgamestudio:registry=https://npm.pkg.github.com\n",
269
- );
270
- const result = scaffoldDeploy({ appName: "npmrcapp", destination: tmp });
271
- expect(result.detected.registryConfigFiles).toEqual([".npmrc"]);
272
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
273
- expect(df).toContain("COPY package.json bun.lock .npmrc ./");
274
- });
275
-
276
- it("a workspaces app renders COPY . . before install (full-tree)", () => {
277
- writeFileSync(
278
- join(tmp, "package.json"),
279
- JSON.stringify({ name: "wsapp", workspaces: ["packages/*"] }),
280
- );
281
- const result = scaffoldDeploy({ appName: "wsapp", destination: tmp });
282
- expect(result.detected.installFromFullTree).toBe(true);
283
- const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
284
- expect(df).toContain("COPY . .\nRUN bun install --frozen-lockfile\nRUN bun run build");
285
- expect(df).not.toContain("COPY package.json bun.lock");
286
- });
287
-
288
- it("a file:/workspace:/link: dependency spec triggers full-tree install without a workspaces field", () => {
289
- writeFileSync(
290
- join(tmp, "package.json"),
291
- JSON.stringify({ name: "localdepapp", dependencies: { "@app/define": "file:./.kumiko" } }),
292
- );
293
- const result = scaffoldDeploy({ appName: "localdepapp", destination: tmp });
294
- expect(result.detected.installFromFullTree).toBe(true);
295
- });
296
- });
297
-
298
- describe("kumiko.deploy config", () => {
299
- it("no kumiko.deploy → migrate-step uses appName as db user and the discover block", () => {
300
- scaffoldDeploy({ appName: "plainapp", destination: tmp });
301
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
302
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
303
- expect(migrate).toContain("postgresql://plainapp:${DB_PASSWORD}@db:5432/plainapp");
304
- expect(migrate).toContain("docker network ls");
305
- expect(migrate).not.toContain("basename");
306
- });
307
-
308
- it('dbUser "kumiko" substitutes only the DB-URL user, db name stays appName', () => {
309
- writeFileSync(
310
- join(tmp, "package.json"),
311
- JSON.stringify({ name: "dbuserapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
312
- );
313
- scaffoldDeploy({ appName: "dbuserapp", destination: tmp });
314
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
315
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
316
- expect(migrate).toContain("postgresql://kumiko:${DB_PASSWORD}@db:5432/dbuserapp");
317
- });
318
-
319
- it('stackNetwork "directory" renders the exact-name block, not the discover block', () => {
320
- writeFileSync(
321
- join(tmp, "package.json"),
322
- JSON.stringify({ name: "dirapp", kumiko: { deploy: { stackNetwork: "directory" } } }),
323
- );
324
- scaffoldDeploy({ appName: "dirapp", destination: tmp });
325
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
326
- expect(migrate).toContain('STACK_NETWORK="$(basename "$PWD")_stack"');
327
- expect(migrate).toContain("docker network inspect");
328
- expect(migrate).not.toContain("docker network ls");
329
- });
330
-
331
- it.each(["a;rm -rf /", "$(id)", "a b", ""])(
332
- "rejects invalid dbUser %j, naming the field",
333
- (dbUser) => {
334
- writeFileSync(
335
- join(tmp, "package.json"),
336
- JSON.stringify({ name: "invaliddbuser", kumiko: { deploy: { dbUser } } }),
337
- );
338
- expect(() => scaffoldDeploy({ appName: "invaliddbuser", destination: tmp })).toThrow(
339
- /dbUser/,
340
- );
341
- },
342
- );
343
-
344
- it("rejects an invalid stackNetwork value", () => {
345
- writeFileSync(
346
- join(tmp, "package.json"),
347
- JSON.stringify({ name: "invalidnet", kumiko: { deploy: { stackNetwork: "foo" } } }),
348
- );
349
- expect(() => scaffoldDeploy({ appName: "invalidnet", destination: tmp })).toThrow(
350
- /stackNetwork/,
351
- );
352
- });
353
-
354
- it("rejects an unknown key under kumiko.deploy (strict)", () => {
355
- writeFileSync(
356
- join(tmp, "package.json"),
357
- JSON.stringify({ name: "unknownkey", kumiko: { deploy: { dbHost: "elsewhere" } } }),
358
- );
359
- expect(() => scaffoldDeploy({ appName: "unknownkey", destination: tmp })).toThrow(/dbHost/);
360
- });
361
- });
362
-
363
- describe("checkDeployDrift", () => {
364
- it("reports missing when no deploy files exist yet", () => {
365
- const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
366
- expect(result.drifted).toHaveLength(3);
367
- expect(result.drifted.every((d) => d.reason === "missing")).toBe(true);
368
- });
369
-
370
- it("reports differs when an on-disk file no longer matches the rendered content", () => {
371
- scaffoldDeploy({ appName: "driftapp", destination: tmp });
372
- writeFileSync(join(tmp, "deploy", "Dockerfile"), "# hand-edited, drifted");
373
- const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
374
- expect(result.drifted).toEqual([
375
- { path: join(tmp, "deploy", "Dockerfile"), reason: "differs" },
376
- ]);
377
- });
378
-
379
- it("reports no drift right after a fresh scaffold", () => {
380
- scaffoldDeploy({ appName: "driftapp", destination: tmp });
381
- const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
382
- expect(result.drifted).toHaveLength(0);
383
- });
384
-
385
- it("reports no drift for a scaffold rendered with a kumiko.deploy config", () => {
386
- writeFileSync(
387
- join(tmp, "package.json"),
388
- JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
389
- );
390
- scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
391
- const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
392
- expect(result.drifted).toHaveLength(0);
393
- });
394
-
395
- it("reports differs on migrate-step.sh after package.json's dbUser changes", () => {
396
- writeFileSync(
397
- join(tmp, "package.json"),
398
- JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
399
- );
400
- scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
401
- writeFileSync(
402
- join(tmp, "package.json"),
403
- JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "otheruser" } } }),
404
- );
405
- const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
406
- expect(result.drifted).toEqual([
407
- { path: join(tmp, "deploy", "migrate-step.sh"), reason: "differs" },
408
- ]);
409
- });
410
-
411
- it("reports differs when the file on disk uses defaults but package.json now says directory", () => {
412
- scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
413
- writeFileSync(
414
- join(tmp, "package.json"),
415
- JSON.stringify({
416
- name: "driftconfigapp",
417
- kumiko: { deploy: { stackNetwork: "directory" } },
418
- }),
419
- );
420
- const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
421
- expect(result.drifted).toEqual([
422
- { path: join(tmp, "deploy", "migrate-step.sh"), reason: "differs" },
423
- ]);
424
- });
425
- });
426
-
427
- describe("malformed package.json", () => {
428
- it("warns + defaults to no private deps and manifests-first install (mis-detection is visible)", () => {
429
- const warn = spyOn(console, "warn").mockImplementation(() => {});
430
- try {
431
- writeFileSync(join(tmp, "package.json"), "{ this is not json");
432
- const result = scaffoldDeploy({ appName: "broken", destination: tmp });
433
- expect(result.detected.hasPrivateGhPackages).toBe(false);
434
- expect(result.detected.installFromFullTree).toBe(false);
435
- expect(warn).toHaveBeenCalledTimes(1);
436
- expect(warn.mock.calls[0]?.[0]).toContain("is not valid JSON");
437
- } finally {
438
- warn.mockRestore();
439
- }
440
- });
441
-
442
- it("keeps a valid kumiko.deploy config even when dependencies has an unexpected shape", () => {
443
- const warn = spyOn(console, "warn").mockImplementation(() => {});
444
- try {
445
- writeFileSync(
446
- join(tmp, "package.json"),
447
- JSON.stringify({
448
- name: "shapeissue",
449
- dependencies: "not-an-object",
450
- kumiko: { deploy: { dbUser: "kumiko" } },
451
- }),
452
- );
453
- const result = scaffoldDeploy({ appName: "shapeissue", destination: tmp });
454
- expect(result.detected.hasPrivateGhPackages).toBe(false);
455
- expect(warn).toHaveBeenCalledTimes(1);
456
- const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
457
- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
458
- expect(migrate).toContain("postgresql://kumiko:${DB_PASSWORD}@db:5432/shapeissue");
459
- } finally {
460
- warn.mockRestore();
461
- }
462
- });
463
- });
464
- });
@@ -1,109 +0,0 @@
1
- // Phase-2 Validator (Plan-Doc create-kumiko-app.md, Risk #1): pinnt dass
2
- // setupTestStack auf persistent DB einen Reboot mit erweitertem Feature-Set
3
- // überlebt — eine neue r.entity ohne Migration-CLI muss die Tabelle anlegen,
4
- // und ein bereits existierendes Entity darf nicht crashen (duplicate CREATE).
5
- //
6
- // Das ist der Pfad den `bun dev` mit KUMIKO_DEV_DB_NAME triggert: der
7
- // Dev-User editiert src/features/notes.ts, `bun --watch` rebootet den Process,
8
- // runDevApp ruft createKumikoServer → setupTestStack mit denselben DB-Name
9
- // auf, und das neue Notes-Entity muss als Tabelle erscheinen ohne dass der
10
- // User `kumiko schema apply` aufgerufen hat. Bricht der Filter in
11
- // push-entity-projection-tables (test-stack.ts:200-209), lügt der Dev-Flow.
12
-
13
- import { afterAll, describe, expect, test } from "bun:test";
14
- import { asRawClient, createDbConnection, tableExists } from "@cosmicdrift/kumiko-framework/db";
15
- import {
16
- createEntity,
17
- createTextField,
18
- defineFeature,
19
- type FeatureDefinition,
20
- } from "@cosmicdrift/kumiko-framework/engine";
21
- import {
22
- pushEntityProjectionTables,
23
- setupTestStack,
24
- type TestStack,
25
- } from "@cosmicdrift/kumiko-framework/stack";
26
- import { generateId } from "@cosmicdrift/kumiko-framework/utils";
27
-
28
- const baseEntity = createEntity({
29
- fields: { title: createTextField({ personal: false, reason: "test_fixture", required: true }) },
30
- table: "phase2_base_thing",
31
- });
32
-
33
- const noteEntity = createEntity({
34
- fields: { title: createTextField({ personal: false, reason: "test_fixture", required: true }) },
35
- table: "phase2_note",
36
- });
37
-
38
- const baseFeature: FeatureDefinition = defineFeature("phase2-base", (r) => {
39
- r.entity("base-thing", baseEntity);
40
- });
41
-
42
- const notesFeature: FeatureDefinition = defineFeature("phase2-notes", (r) => {
43
- r.entity("note", noteEntity);
44
- });
45
-
46
- const dbName = `kumiko_phase2_${generateId().slice(-8)}`;
47
-
48
- afterAll(async () => {
49
- const base = process.env["TEST_DATABASE_URL"];
50
- if (!base) return;
51
- const adminUrl = base.replace(/\/[^/]+$/, "/postgres");
52
- const admin = createDbConnection(adminUrl, { maxConnections: 1 });
53
- try {
54
- await asRawClient(admin.db).unsafe(`DROP DATABASE IF EXISTS "${dbName}"`);
55
- } finally {
56
- await admin.close();
57
- }
58
- });
59
-
60
- describe("scaffold dev cycle (Phase 2 Validator)", () => {
61
- test("persistent reboot with added entity creates only the new table", async () => {
62
- const base = process.env["TEST_DATABASE_URL"];
63
- if (!base) throw new Error("TEST_DATABASE_URL required");
64
-
65
- // Boot 1: only base feature, persistent DB.
66
- const stack1: TestStack = await setupTestStack({
67
- features: [baseFeature],
68
- dbName,
69
- persistentDb: true,
70
- });
71
- try {
72
- await pushEntityProjectionTables(stack1, stack1.registry);
73
- expect(await tableExists(stack1.db, "public.phase2_base_thing")).toBe(true);
74
- expect(await tableExists(stack1.db, "public.phase2_note")).toBe(false);
75
- } finally {
76
- await stack1.cleanup();
77
- }
78
-
79
- // Boot 2: base + notes — simulates the Dev-User edit + bun --watch reboot.
80
- // pushEntityProjectionTables MUSS phase2_base_thing skippen (existiert)
81
- // und phase2_note neu anlegen — sonst crash auf duplicate CREATE TABLE
82
- // oder die Notes-Tabelle fehlt.
83
- const stack2: TestStack = await setupTestStack({
84
- features: [baseFeature, notesFeature],
85
- dbName,
86
- persistentDb: true,
87
- });
88
- try {
89
- await pushEntityProjectionTables(stack2, stack2.registry);
90
- expect(await tableExists(stack2.db, "public.phase2_base_thing")).toBe(true);
91
- expect(await tableExists(stack2.db, "public.phase2_note")).toBe(true);
92
- } finally {
93
- await stack2.cleanup();
94
- }
95
-
96
- // Boot 3 with same feature-set must remain idempotent (no double-CREATE).
97
- const stack3: TestStack = await setupTestStack({
98
- features: [baseFeature, notesFeature],
99
- dbName,
100
- persistentDb: true,
101
- });
102
- try {
103
- await pushEntityProjectionTables(stack3, stack3.registry);
104
- expect(await tableExists(stack3.db, "public.phase2_note")).toBe(true);
105
- } finally {
106
- await stack3.cleanup();
107
- }
108
- });
109
- });