@cosmicdrift/kumiko-dev-server 0.152.0 → 0.153.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/package.json +5 -8
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/compose-stacks.test.ts +1 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/walkthrough.integration.test.ts +1 -1
- package/src/build.ts +1 -1
- package/src/create-kumiko-server.ts +12 -6
- package/src/index.ts +4 -12
- package/src/run-dev-app.ts +12 -8
- package/src/scaffold-app.ts +9 -4
- package/src/schema-apply.ts +4 -1
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +4 -1
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -278
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -128
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/pii-boot-gate.test.ts +0 -68
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -915
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/apply-boot-seeds.ts +0 -19
- package/src/boot/boot-crypto.ts +0 -82
- package/src/boot/job-run-logger.ts +0 -51
- package/src/build-prod-bundle.ts +0 -692
- package/src/compose-features.ts +0 -164
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -30
- package/src/pii-boot-gate.ts +0 -62
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -241
- package/src/run-prod-app-static-files.ts +0 -273
- package/src/run-prod-app.ts +0 -1158
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// injectSchema teilt sich dev-server (every HTML response) und prod-
|
|
2
|
-
// server (static-fallback index.html) Pfad. Bug hier wäre stiller
|
|
3
|
-
// Production-Fail: createKumikoApp findet `window.__KUMIKO_SCHEMA__`
|
|
4
|
-
// nicht und mountet leer. Tests pinnen die Idempotenz + die zwei
|
|
5
|
-
// Insertion-Punkte (vor /client.js-Tag oder vor </body>).
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { injectSchema } from "../inject-schema";
|
|
9
|
-
|
|
10
|
-
const SCHEMA = '{"features":[]}';
|
|
11
|
-
const TAG = `<script>window.__KUMIKO_SCHEMA__=${SCHEMA};</script>`;
|
|
12
|
-
|
|
13
|
-
describe("injectSchema", () => {
|
|
14
|
-
test("HTML mit /client.js-Tag: Schema-Tag wird DAVOR eingefügt", () => {
|
|
15
|
-
const html = '<html><body><script src="/client.js" defer></script></body></html>';
|
|
16
|
-
const out = injectSchema(html, SCHEMA);
|
|
17
|
-
expect(out).toContain(TAG);
|
|
18
|
-
// Schema MUSS vor dem Client-Bundle stehen — sonst läuft
|
|
19
|
-
// createKumikoApp() bevor window.__KUMIKO_SCHEMA__ gesetzt ist.
|
|
20
|
-
expect(out.indexOf(TAG)).toBeLessThan(out.indexOf('<script src="/client.js"'));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("HTML ohne /client.js-Tag: Schema-Tag wird vor </body> eingefügt", () => {
|
|
24
|
-
const html = "<html><body><div id=root></div></body></html>";
|
|
25
|
-
const out = injectSchema(html, SCHEMA);
|
|
26
|
-
expect(out).toContain(TAG);
|
|
27
|
-
expect(out.indexOf(TAG)).toBeLessThan(out.indexOf("</body>"));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("HTML ohne </body>: Schema-Tag wird angehängt (defensiver Fallback)", () => {
|
|
31
|
-
const html = "<div>fragment</div>";
|
|
32
|
-
const out = injectSchema(html, SCHEMA);
|
|
33
|
-
expect(out.endsWith(TAG)).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("Idempotent: bei bereits injectem Schema kein zweiter Tag", () => {
|
|
37
|
-
const html = `<html><body>${TAG}</body></html>`;
|
|
38
|
-
const out = injectSchema(html, '{"features":[{"differentSchema":true}]}');
|
|
39
|
-
// Original-Tag bleibt, kein zweiter Tag hinzugefügt — der Marker-
|
|
40
|
-
// Check verhindert sonst stacking-Tags bei repeated reads.
|
|
41
|
-
expect(out).toBe(html);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("Schema mit Komplex-Daten (entities + screens) bleibt valides JS", () => {
|
|
45
|
-
const complex = JSON.stringify({
|
|
46
|
-
features: [
|
|
47
|
-
{
|
|
48
|
-
featureName: "items",
|
|
49
|
-
entities: { item: { fields: { title: { type: "text" } } } },
|
|
50
|
-
screens: [{ id: "list", type: "entityList", entity: "item", columns: ["title"] }],
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
});
|
|
54
|
-
const html = '<html><body><script src="/client.js"></script></body></html>';
|
|
55
|
-
const out = injectSchema(html, complex);
|
|
56
|
-
// Sanity: das injected Skript muss valid JS sein (Object-Literal-
|
|
57
|
-
// Syntax, keine HTML-Reserved-Chars im JSON die den <script>-Block
|
|
58
|
-
// brechen würden — JSON.stringify entkommt /, < usw. nicht, aber
|
|
59
|
-
// die Standard-Chars die wir hier nutzen sind unkritisch).
|
|
60
|
-
expect(out).toContain(`window.__KUMIKO_SCHEMA__=${complex}`);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
|
-
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import { assertPiiBootInvariants } from "../pii-boot-gate";
|
|
5
|
-
|
|
6
|
-
const piiFeature = defineFeature("gate-pii", (r) => {
|
|
7
|
-
r.entity(
|
|
8
|
-
"person",
|
|
9
|
-
createEntity({
|
|
10
|
-
table: "read_gate_persons",
|
|
11
|
-
fields: { email: createTextField({ required: true, pii: true, lookupable: true }) },
|
|
12
|
-
}),
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
const plainFeature = defineFeature("gate-plain", (r) => {
|
|
16
|
-
r.entity(
|
|
17
|
-
"thing",
|
|
18
|
-
createEntity({ table: "read_gate_things", fields: { name: createTextField() } }),
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const kms = new InMemoryKmsAdapter();
|
|
23
|
-
const KEY = Buffer.alloc(32, 7).toString("base64");
|
|
24
|
-
|
|
25
|
-
describe("assertPiiBootInvariants — prod", () => {
|
|
26
|
-
test("PII without a KMS aborts boot", () => {
|
|
27
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod" })).toThrow(
|
|
28
|
-
/BOOT ABORTED.*PLAINTEXT.*allowPlaintextPii/s,
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("explicit allowPlaintextPii downgrades to a warning", () => {
|
|
33
|
-
expect(() =>
|
|
34
|
-
assertPiiBootInvariants([piiFeature], {
|
|
35
|
-
mode: "prod",
|
|
36
|
-
allowPlaintextPii: "kms rollout pending, infra#188",
|
|
37
|
-
}),
|
|
38
|
-
).not.toThrow();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("KMS + blindIndexKey boots", () => {
|
|
42
|
-
expect(() =>
|
|
43
|
-
assertPiiBootInvariants([piiFeature], { mode: "prod", kms, blindIndexKey: KEY }),
|
|
44
|
-
).not.toThrow();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("KMS without blindIndexKey aborts when lookupable fields exist", () => {
|
|
48
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod", kms })).toThrow(
|
|
49
|
-
/blindIndexKey/,
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("no PII entities → nothing to gate", () => {
|
|
54
|
-
expect(() => assertPiiBootInvariants([plainFeature], { mode: "prod" })).not.toThrow();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("assertPiiBootInvariants — dev", () => {
|
|
59
|
-
test("PII without a KMS only warns in dev", () => {
|
|
60
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev" })).not.toThrow();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("KMS without blindIndexKey aborts in dev too (lookups broken in any mode)", () => {
|
|
64
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev", kms })).toThrow(
|
|
65
|
-
/blindIndexKey/,
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Relocation-Test für den @source-Layout-Fix (#359).
|
|
2
|
-
//
|
|
3
|
-
// Der Bug: renderer-web/src/styles.css scannte seine eigenen Shell-Klassen
|
|
4
|
-
// über einen MONOREPO-relativen @source (`../../renderer-web/src`). Im
|
|
5
|
-
// Workspace löst der via Symlink auf (grün), in einem Standalone-Consumer
|
|
6
|
-
// (echtes node_modules ohne Monorepo-Geschwister) findet er nichts → unstyled
|
|
7
|
-
// prod (15KB statt 48KB). Der Fix macht die Zeile self-relativ (`./`), weil
|
|
8
|
-
// das Paket `src` shippt.
|
|
9
|
-
//
|
|
10
|
-
// Am REALEN Ort der styles.css sind `./` und `../../renderer-web/src`
|
|
11
|
-
// identisch — ein Test dort wäre grün mit Bug UND Fix. Der Diskriminator ist
|
|
12
|
-
// RELOCATION: wir kopieren die echte styles.css an einen Ort, an dem der alte
|
|
13
|
-
// Pfad tot ist und nur `./` greift. Der Ort liegt unter dem Repo-Root, damit
|
|
14
|
-
// `@import "tailwindcss"` / `react-day-picker` weiter über node_modules
|
|
15
|
-
// auflösen. Braucht Bun (Bun.spawn im Tailwind-One-Shot) — sonst silent skip.
|
|
16
|
-
|
|
17
|
-
import { describe, expect, test } from "bun:test";
|
|
18
|
-
import { execFileSync } from "node:child_process";
|
|
19
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
|
-
import { dirname, join, resolve } from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { runTailwindOnce } from "../build-prod-bundle";
|
|
23
|
-
|
|
24
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const REPO_ROOT = resolve(__dirname, "../../../..");
|
|
26
|
-
const RENDERER_WEB_STYLES = resolve(__dirname, "../../../renderer-web/src/styles.css");
|
|
27
|
-
const SELF_SOURCE = '@source "./**/*.{ts,tsx}";';
|
|
28
|
-
const MONOREPO_SOURCE = '@source "../../renderer-web/src/**/*.{ts,tsx}";';
|
|
29
|
-
|
|
30
|
-
function bunAvailable(): boolean {
|
|
31
|
-
try {
|
|
32
|
-
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
33
|
-
return true;
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("renderer-web styles.css @source relocation (#359)", () => {
|
|
40
|
-
// skipIf statt `if (!bunAvailable()) return;` — sonst meldet der Runner den
|
|
41
|
-
// Test grün, obwohl er nie lief (silent-pass verschleiert fehlende Coverage).
|
|
42
|
-
test.skipIf(!bunAvailable())(
|
|
43
|
-
"self-relative @source scans the shell standalone; monorepo path would not",
|
|
44
|
-
async () => {
|
|
45
|
-
// Tailwind v4 auto-scannt nur das cwd-Verzeichnis (empirisch bestätigt:
|
|
46
|
-
// das Verzeichnis der Input-CSS wird NICHT automatisch gescannt). Im echten
|
|
47
|
-
// Standalone-Consumer ist renderer-web zudem in node_modules (gitignored) →
|
|
48
|
-
// ebenfalls nicht auto-gescannt. Die Shell-Klassen erreicht also NUR der
|
|
49
|
-
// explizite @source der styles.css. Hier mirrorn wir das robust, ohne
|
|
50
|
-
// gitignore-/node_modules-Semantik: das Paket-`src` (mit der Shell-Probe)
|
|
51
|
-
// liegt AUSSERHALB des Build-cwd — nur der self-relative `@source "./**"`
|
|
52
|
-
// der relozierten styles.css erreicht es, der monorepo-Pfad ist tot.
|
|
53
|
-
// Temp unter REPO_ROOT, damit @import "tailwindcss"/react-day-picker via
|
|
54
|
-
// node_modules auflösen.
|
|
55
|
-
const dir = await mkdtemp(join(REPO_ROOT, ".reloc-rendererweb-"));
|
|
56
|
-
const buildCwd = join(dir, "app");
|
|
57
|
-
const pkgSrc = join(dir, "pkg");
|
|
58
|
-
try {
|
|
59
|
-
const realCss = await readFile(RENDERER_WEB_STYLES, "utf8");
|
|
60
|
-
// Guard: der Fix MUSS in der Quelle stehen, sonst testet die Relocation nichts.
|
|
61
|
-
expect(realCss).toContain(SELF_SOURCE);
|
|
62
|
-
|
|
63
|
-
await mkdir(buildCwd, { recursive: true });
|
|
64
|
-
await mkdir(join(pkgSrc, "layout"), { recursive: true });
|
|
65
|
-
await writeFile(
|
|
66
|
-
join(pkgSrc, "layout/probe.tsx"),
|
|
67
|
-
`export const P = () => <div className="min-h-screen" />;\n`,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
await writeFile(join(pkgSrc, "styles.css"), realCss);
|
|
71
|
-
const fixed = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
|
|
72
|
-
expect(fixed).toContain("min-h-screen");
|
|
73
|
-
|
|
74
|
-
// Diskriminator: mit dem ALTEN monorepo-relativen @source ist die Probe
|
|
75
|
-
// an diesem relozierten Ort unerreichbar → Sentinel fehlt. Beweist, dass
|
|
76
|
-
// der Fix kein No-op ist (der Test würde bei einem Revert rot).
|
|
77
|
-
await writeFile(join(pkgSrc, "styles.css"), realCss.replace(SELF_SOURCE, MONOREPO_SOURCE));
|
|
78
|
-
const buggy = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
|
|
79
|
-
expect(buggy).not.toContain("min-h-screen");
|
|
80
|
-
} finally {
|
|
81
|
-
await rm(dir, { recursive: true, force: true });
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Unit-Test für den CSS-Completeness-Guard (#359): nach dem Tailwind-One-Shot
|
|
2
|
-
// prüft der Build, ob das Output-CSS die renderer-web-Shell-Sentinel-Klasse
|
|
3
|
-
// enthält — aber NUR wenn auf das gepackte renderer-web-styles.css
|
|
4
|
-
// zurückgefallen wurde. Fehlt sie dort, würde prod unstyled rendern → laut
|
|
5
|
-
// failen mit Hinweis auf src/styles.css.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { assertRendererWebShellPresent } from "../build-prod-bundle";
|
|
9
|
-
|
|
10
|
-
const fallback = {
|
|
11
|
-
path: "/app/node_modules/renderer-web/src/styles.css",
|
|
12
|
-
isRendererWebFallback: true,
|
|
13
|
-
};
|
|
14
|
-
const appOwned = { path: "/app/src/styles.css", isRendererWebFallback: false };
|
|
15
|
-
|
|
16
|
-
describe("assertRendererWebShellPresent (#359 CSS-completeness guard)", () => {
|
|
17
|
-
test("fallback + fehlende Shell-Sentinel-Klasse → wirft mit src/styles.css-Hinweis", () => {
|
|
18
|
-
expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
|
|
19
|
-
/min-h-screen/,
|
|
20
|
-
);
|
|
21
|
-
expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
|
|
22
|
-
/src\/styles\.css/,
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("fallback + Shell-Sentinel vorhanden → kein Throw", () => {
|
|
27
|
-
expect(() =>
|
|
28
|
-
assertRendererWebShellPresent(".min-h-screen{min-height:100vh}", fallback),
|
|
29
|
-
).not.toThrow();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("app-eigenes styles.css (kein Fallback) → nie asserten, auch ohne Sentinel", () => {
|
|
33
|
-
expect(() => assertRendererWebShellPresent("", appOwned)).not.toThrow();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// requireEnv — the missing-var error message must match the actual caller.
|
|
2
|
-
// Regression (728/2): the advice text was hardcoded to prod/container
|
|
3
|
-
// wording ("Coolify secrets") regardless of the `context` param, so a
|
|
4
|
-
// runDevApp caller got misleading production-deploy instructions for a
|
|
5
|
-
// missing local .env var.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { requireEnv } from "../run-prod-app";
|
|
9
|
-
|
|
10
|
-
describe("requireEnv", () => {
|
|
11
|
-
test("default context (runProdApp) gives container/production advice", () => {
|
|
12
|
-
expect(() => requireEnv("MISSING_VAR", {})).toThrow(/container env.*Coolify secrets/);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("runDevApp context gives local .env advice, not production advice", () => {
|
|
16
|
-
expect(() => requireEnv("MISSING_VAR", {}, "runDevApp")).toThrow(
|
|
17
|
-
/\.env \/ shell before running the dev server/,
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("runDevApp context error does NOT mention Coolify", () => {
|
|
22
|
-
try {
|
|
23
|
-
requireEnv("MISSING_VAR", {}, "runDevApp");
|
|
24
|
-
throw new Error("expected requireEnv to throw");
|
|
25
|
-
} catch (err) {
|
|
26
|
-
expect(String(err)).not.toContain("Coolify");
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { type RunProdAppAuthOptions, resolveAuthMail } from "../run-prod-app";
|
|
3
|
-
|
|
4
|
-
// Pins the auth.mail convenience (resolveAuthMail): one mail block expands
|
|
5
|
-
// into the four explicit flow setups built from DEFAULT_AUTH_PATHS, the
|
|
6
|
-
// null-transport guard (no SMTP_HOST → flows stay unwired), and explicit
|
|
7
|
-
// per-flow setups winning over the mail default.
|
|
8
|
-
|
|
9
|
-
const admin: RunProdAppAuthOptions["admin"] = {
|
|
10
|
-
email: "admin@example.com",
|
|
11
|
-
password: "pw-long-enough",
|
|
12
|
-
displayName: "Admin",
|
|
13
|
-
memberships: [],
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const withMail: RunProdAppAuthOptions = {
|
|
17
|
-
admin,
|
|
18
|
-
mail: { baseUrl: "https://app.example.com", appName: "Test" },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
describe("resolveAuthMail", () => {
|
|
22
|
-
test("no mail block → auth returned unchanged", () => {
|
|
23
|
-
const noMail: RunProdAppAuthOptions = { admin };
|
|
24
|
-
const out = resolveAuthMail(noMail, "secret", { SMTP_HOST: "localhost" });
|
|
25
|
-
expect(out.passwordReset).toBeUndefined();
|
|
26
|
-
expect(out.signup).toBeUndefined();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("mail + SMTP_HOST → all four flows wired from DEFAULT_AUTH_PATHS", () => {
|
|
30
|
-
const out = resolveAuthMail(withMail, "secret", { SMTP_HOST: "localhost" });
|
|
31
|
-
// All four flows carry appUrl as feature options — each handler mails via
|
|
32
|
-
// delivery (ctx.notify); no callback wiring remains.
|
|
33
|
-
expect(out.passwordReset?.appUrl).toBe("https://app.example.com/reset-password");
|
|
34
|
-
expect(out.emailVerification?.appUrl).toBe("https://app.example.com/verify-email");
|
|
35
|
-
expect(out.signup?.appUrl).toBe("https://app.example.com/signup/complete");
|
|
36
|
-
expect(out.invite?.appUrl).toBe("https://app.example.com/invite/accept");
|
|
37
|
-
expect(out.passwordReset?.hmacSecret).toBe("secret");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("mail but NO SMTP_HOST → null-transport guard, flows stay unwired", () => {
|
|
41
|
-
const out = resolveAuthMail(withMail, "secret", {});
|
|
42
|
-
expect(out.passwordReset).toBeUndefined();
|
|
43
|
-
expect(out.signup).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("explicit per-flow setup wins over the mail default", () => {
|
|
47
|
-
const explicit: RunProdAppAuthOptions = {
|
|
48
|
-
...withMail,
|
|
49
|
-
passwordReset: {
|
|
50
|
-
hmacSecret: "h",
|
|
51
|
-
appUrl: "https://custom.example.com/pw",
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
const out = resolveAuthMail(explicit, "secret", { SMTP_HOST: "localhost" });
|
|
55
|
-
expect(out.passwordReset?.appUrl).toBe("https://custom.example.com/pw");
|
|
56
|
-
// other flows still come from the mail default
|
|
57
|
-
expect(out.signup?.appUrl).toBe("https://app.example.com/signup/complete");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("paths override only affects the named path", () => {
|
|
61
|
-
const pathsOverride: RunProdAppAuthOptions = {
|
|
62
|
-
admin,
|
|
63
|
-
mail: { baseUrl: "https://app.example.com", paths: { resetPassword: "/pw" } },
|
|
64
|
-
};
|
|
65
|
-
const out = resolveAuthMail(pathsOverride, "secret", { SMTP_HOST: "localhost" });
|
|
66
|
-
expect(out.passwordReset?.appUrl).toBe("https://app.example.com/pw");
|
|
67
|
-
expect(out.emailVerification?.appUrl).toBe("https://app.example.com/verify-email");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// Unit-Tests für resolveTailwindCli — die zwei Failure-Branches sind
|
|
2
|
-
// genau das, was den dev-server-Crash bei flakigem Netz verhindert:
|
|
3
|
-
// kein Bun → undefined, Package nicht installiert → undefined.
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import { canResolveTailwindStylesheet, resolveTailwindCli } from "../resolve-tailwind-cli";
|
|
7
|
-
|
|
8
|
-
describe("resolveTailwindCli", () => {
|
|
9
|
-
test("ohne Bun-Resolver → undefined (silent skip)", () => {
|
|
10
|
-
const out = resolveTailwindCli({ bun: undefined, cwd: "/somewhere" });
|
|
11
|
-
expect(out).toBeUndefined();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("Bun.resolveSync wirft → undefined", () => {
|
|
15
|
-
const out = resolveTailwindCli({
|
|
16
|
-
bun: {
|
|
17
|
-
resolveSync: () => {
|
|
18
|
-
throw new Error("module not found");
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
cwd: "/somewhere",
|
|
22
|
-
});
|
|
23
|
-
expect(out).toBeUndefined();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("Bun.resolveSync liefert package.json → absoluter Bin-Pfad", () => {
|
|
27
|
-
const out = resolveTailwindCli({
|
|
28
|
-
bun: {
|
|
29
|
-
resolveSync: () => "/repo/node_modules/@tailwindcss/cli/package.json",
|
|
30
|
-
},
|
|
31
|
-
cwd: "/repo",
|
|
32
|
-
});
|
|
33
|
-
expect(out).toBe("/repo/node_modules/@tailwindcss/cli/dist/index.mjs");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("Bun-Resolver wird mit korrektem id und cwd aufgerufen", () => {
|
|
37
|
-
const calls: Array<{ id: string; from: string }> = [];
|
|
38
|
-
resolveTailwindCli({
|
|
39
|
-
bun: {
|
|
40
|
-
resolveSync: (id, from) => {
|
|
41
|
-
calls.push({ id, from });
|
|
42
|
-
return "/x/node_modules/@tailwindcss/cli/package.json";
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
cwd: "/some/working/dir",
|
|
46
|
-
});
|
|
47
|
-
expect(calls).toEqual([{ id: "@tailwindcss/cli/package.json", from: "/some/working/dir" }]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("canResolveTailwindStylesheet", () => {
|
|
52
|
-
test("tailwindcss resolvable from entry dir → true", () => {
|
|
53
|
-
const out = canResolveTailwindStylesheet("/repo/packages/app/src/styles.css", {
|
|
54
|
-
bun: {
|
|
55
|
-
resolveSync: (id, from) => {
|
|
56
|
-
if (id === "tailwindcss" && from === "/repo/packages/app/src") {
|
|
57
|
-
return "/repo/node_modules/tailwindcss/index.css";
|
|
58
|
-
}
|
|
59
|
-
throw new Error("not found");
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
cwd: "/repo/packages/app",
|
|
63
|
-
});
|
|
64
|
-
expect(out).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("tailwindcss not resolvable from entry dir → false", () => {
|
|
68
|
-
const out = canResolveTailwindStylesheet(
|
|
69
|
-
"/cache/@cosmicdrift/kumiko-renderer-web/src/styles.css",
|
|
70
|
-
{
|
|
71
|
-
bun: {
|
|
72
|
-
resolveSync: () => {
|
|
73
|
-
throw new Error("not found");
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
cwd: "/tmp/minimal-fixture",
|
|
77
|
-
},
|
|
78
|
-
);
|
|
79
|
-
expect(out).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// Regression: the boot-path must read the injected `envSource`, not the real
|
|
2
|
-
// process.env. Boot-mode (KUMIKO_DRY_RUN_ENV=boot) validates wiring + builds
|
|
3
|
-
// the registry, then tears down the lazy DB/Redis clients before any socket
|
|
4
|
-
// opens — so this runs without a real Postgres/Redis (same as the CI boot
|
|
5
|
-
// smoke). Before the fix, requireEnv/readEnv read process.env directly, so the
|
|
6
|
-
// required-var test would throw "required env var DATABASE_URL is missing" and
|
|
7
|
-
// the PORT test would bind the default instead of the injected port.
|
|
8
|
-
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import {
|
|
11
|
-
createBooleanField,
|
|
12
|
-
createEntity,
|
|
13
|
-
createTextField,
|
|
14
|
-
defineFeature,
|
|
15
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
-
import { z } from "zod";
|
|
17
|
-
import { runProdApp } from "../run-prod-app";
|
|
18
|
-
|
|
19
|
-
const probeEntity = createEntity({
|
|
20
|
-
fields: {
|
|
21
|
-
name: createTextField({ required: true }),
|
|
22
|
-
active: createBooleanField({ default: true }),
|
|
23
|
-
},
|
|
24
|
-
table: "env_source_probe",
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const probeFeature = defineFeature("env-source-probe", (r) => {
|
|
28
|
-
r.entity("widget", probeEntity);
|
|
29
|
-
r.queryHandler({
|
|
30
|
-
name: "ping",
|
|
31
|
-
schema: z.object({}),
|
|
32
|
-
access: { roles: ["anonymous"] },
|
|
33
|
-
handler: async () => ({ pong: true }),
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Cleared from process.env so the test fully controls config via envSource.
|
|
38
|
-
// DATABASE_URL/REDIS_URL/JWT_SECRET are required (their read throws pre-fix);
|
|
39
|
-
// PORT is non-throwing, cleared only so ambient PORT can't mask the second
|
|
40
|
-
// test's "PORT comes from envSource" assertion.
|
|
41
|
-
const CLEARED_VARS = ["DATABASE_URL", "REDIS_URL", "JWT_SECRET", "PORT"] as const;
|
|
42
|
-
|
|
43
|
-
const DUMMY_ENV = {
|
|
44
|
-
KUMIKO_DRY_RUN_ENV: "boot",
|
|
45
|
-
DATABASE_URL: "postgres://smoke:smoke@127.0.0.1:1/smoke",
|
|
46
|
-
REDIS_URL: "redis://127.0.0.1:1",
|
|
47
|
-
JWT_SECRET: "smokesmokesmokesmokesmokesmokesmokesmoke",
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
describe("runProdApp boot-mode env-source", () => {
|
|
51
|
-
const saved: Record<string, string | undefined> = {};
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
for (const k of CLEARED_VARS) {
|
|
55
|
-
saved[k] = process.env[k];
|
|
56
|
-
delete process.env[k];
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
for (const k of CLEARED_VARS) {
|
|
62
|
-
if (saved[k] === undefined) delete process.env[k];
|
|
63
|
-
else process.env[k] = saved[k];
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("boots from injected envSource even when process.env lacks the required vars", async () => {
|
|
68
|
-
const logs: string[] = [];
|
|
69
|
-
const originalLog = console.log;
|
|
70
|
-
console.log = (...args: unknown[]) => {
|
|
71
|
-
logs.push(args.map(String).join(" "));
|
|
72
|
-
};
|
|
73
|
-
let handle: Awaited<ReturnType<typeof runProdApp>>;
|
|
74
|
-
try {
|
|
75
|
-
handle = await runProdApp({
|
|
76
|
-
features: [probeFeature],
|
|
77
|
-
autoListen: false,
|
|
78
|
-
migrations: false,
|
|
79
|
-
// REDIS_URL points at an unreachable port — boot-mode must NOT
|
|
80
|
-
// construct the (eager) Redis client, so this never tries to connect.
|
|
81
|
-
envSource: { ...DUMMY_ENV },
|
|
82
|
-
});
|
|
83
|
-
} finally {
|
|
84
|
-
console.log = originalLog;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Boot-mode with an injected envSource returns an inert dry-run handle.
|
|
88
|
-
expect(handle).toBeDefined();
|
|
89
|
-
expect(typeof handle.stop).toBe("function");
|
|
90
|
-
// The registry was built + validated before any connection was opened.
|
|
91
|
-
expect(logs.some((line) => line.includes("boot validation OK"))).toBe(true);
|
|
92
|
-
await handle.stop();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("boot-mode constructs no eager Redis client — kein TCP-Connect auf REDIS_URL", async () => {
|
|
96
|
-
// Kern-Garantie (224/2), als Netzwerk-Beweis statt Konstruktor-Spy:
|
|
97
|
-
// REDIS_URL zeigt auf einen lokalen Listener — `new Redis(...)`
|
|
98
|
-
// connectet eager, der Boot-Exit MUSS also vorher liegen, sonst
|
|
99
|
-
// zählt der Listener eine Connection.
|
|
100
|
-
let connections = 0;
|
|
101
|
-
const listener = Bun.listen({
|
|
102
|
-
hostname: "127.0.0.1",
|
|
103
|
-
port: 0,
|
|
104
|
-
socket: {
|
|
105
|
-
open(socket) {
|
|
106
|
-
connections += 1;
|
|
107
|
-
socket.end();
|
|
108
|
-
},
|
|
109
|
-
data() {},
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const originalLog = console.log;
|
|
114
|
-
console.log = () => {};
|
|
115
|
-
try {
|
|
116
|
-
const handle = await runProdApp({
|
|
117
|
-
features: [probeFeature],
|
|
118
|
-
autoListen: false,
|
|
119
|
-
migrations: false,
|
|
120
|
-
envSource: { ...DUMMY_ENV, REDIS_URL: `redis://127.0.0.1:${listener.port}` },
|
|
121
|
-
});
|
|
122
|
-
await handle.stop();
|
|
123
|
-
} finally {
|
|
124
|
-
console.log = originalLog;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Ein eager Connect wäre bereits beim runProdApp-await passiert;
|
|
128
|
-
// kleine Nachfrist für asynchrone Socket-Anläufe.
|
|
129
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
130
|
-
listener.stop(true);
|
|
131
|
-
expect(connections).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("resolves PORT from envSource, not process.env", async () => {
|
|
135
|
-
const logs: string[] = [];
|
|
136
|
-
const originalLog = console.log;
|
|
137
|
-
console.log = (...args: unknown[]) => {
|
|
138
|
-
logs.push(args.map(String).join(" "));
|
|
139
|
-
};
|
|
140
|
-
try {
|
|
141
|
-
const handle = await runProdApp({
|
|
142
|
-
features: [probeFeature],
|
|
143
|
-
autoListen: false,
|
|
144
|
-
migrations: false,
|
|
145
|
-
envSource: { ...DUMMY_ENV, PORT: "8123" },
|
|
146
|
-
});
|
|
147
|
-
await handle.stop();
|
|
148
|
-
} finally {
|
|
149
|
-
console.log = originalLog;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// The boot logs "booting Kumiko stack on port <port>" — pre-fix this read
|
|
153
|
-
// process.env["PORT"] (deleted here) and would log the 3000 default.
|
|
154
|
-
expect(logs.some((line) => line.includes("port 8123"))).toBe(true);
|
|
155
|
-
expect(logs.some((line) => line.includes("port 3000"))).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Spec-Tests für die Bun.serve-Options + Heartbeat-Cadence. Diese
|
|
2
|
-
// Konstanten/Defaults haben Live-Bugs verursacht und sind 1-Zeile-
|
|
3
|
-
// revertierbar — die Tests pinsen Intent + akzeptablen Range gegen
|
|
4
|
-
// "looks like a leak"-Reverts oder "bisschen rauf, sollte reichen"-
|
|
5
|
-
// Tweaks.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import { SSE_HEARTBEAT_INTERVAL_MS } from "@cosmicdrift/kumiko-framework/api";
|
|
9
|
-
import { buildBunServeOptions } from "../run-prod-app";
|
|
10
|
-
|
|
11
|
-
describe("Bun.serve options for production", () => {
|
|
12
|
-
test("idleTimeout is 0 (disabled) — required for SSE long-lived connections", () => {
|
|
13
|
-
// Bun.serve default ist 10s — ohne Override killt das SSE-Streams
|
|
14
|
-
// mit halbem HTTP/2-RST_STREAM. Spec ist "disabled"; jeder andere
|
|
15
|
-
// Wert (auch ein vermeintlich "großzügiges" 60) bricht SSE sobald
|
|
16
|
-
// ein Client den Tab im Hintergrund hat (kein Heartbeat-Read auf
|
|
17
|
-
// Browser-Seite, Idle-Timer feuert).
|
|
18
|
-
const opts = buildBunServeOptions(0, () => new Response("ok"));
|
|
19
|
-
expect(opts.idleTimeout).toBe(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("port + fetch werden 1:1 durchgereicht", () => {
|
|
23
|
-
const fetchHandler = (_req: Request) => new Response("test");
|
|
24
|
-
const opts = buildBunServeOptions(3000, fetchHandler);
|
|
25
|
-
expect(opts.port).toBe(3000);
|
|
26
|
-
expect(opts.fetch).toBe(fetchHandler);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("SSE heartbeat cadence", () => {
|
|
31
|
-
test("liegt unter dem strengsten Edge-Idle-Timeout (Cloudflare 100s)", () => {
|
|
32
|
-
// Bun.serve idleTimeout ist disabled (siehe buildBunServeOptions),
|
|
33
|
-
// damit fällt der Bun-default-10s-Layer raus. Die strengsten
|
|
34
|
-
// verbleibenden Layer sind CDN/LB-Edges:
|
|
35
|
-
// - Cloudflare Edge: 100 s ← strengster realistischer Layer
|
|
36
|
-
// - AWS ALB: 60 s
|
|
37
|
-
// - Nginx default: keep-alive 60 s, aber nicht für Streams
|
|
38
|
-
// Heartbeat muss DEUTLICH darunter liegen damit auch ein verschluckter
|
|
39
|
-
// Frame nicht zur Connection-Death führt — Faktor 5 ist konservativ.
|
|
40
|
-
expect(SSE_HEARTBEAT_INTERVAL_MS).toBeLessThan(60_000);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("liegt nicht zu hoch — DefenseInDepth gegen Bun-Default-Drift", () => {
|
|
44
|
-
// Wenn jemand idleTimeout: 0 wieder rausnimmt (Audit, Linter,
|
|
45
|
-
// Misverständnis), darf der Heartbeat nicht der einzige
|
|
46
|
-
// Schutzwall sein der unter dem 10s-Bun-default liegt. ≤30s
|
|
47
|
-
// gibt Cushion + bleibt deutlich unter Bun-default-10s × 3.
|
|
48
|
-
expect(SSE_HEARTBEAT_INTERVAL_MS).toBeLessThanOrEqual(30_000);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("ist >= 5 s — Frequency-Wall gegen versehentliches Network-Spam", () => {
|
|
52
|
-
// 1000 anonyme Viewer × Heartbeat-Frequency darf den Server nicht
|
|
53
|
-
// mit Frames fluten. 5s = 200 frames/s pro 1000 Clients ist OK.
|
|
54
|
-
// Unter 5s wäre eher ein "tippfehler" als bewusste Wahl.
|
|
55
|
-
expect(SSE_HEARTBEAT_INTERVAL_MS).toBeGreaterThanOrEqual(5_000);
|
|
56
|
-
});
|
|
57
|
-
});
|