@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.
- 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,143 +0,0 @@
|
|
|
1
|
-
// scaffoldFeature tests — verify the CLI's `create` subcommand produces
|
|
2
|
-
// a valid, parsable feature workspace. Strategy: scaffold into an
|
|
3
|
-
// in-tmpdir destination, then read the output back and assert:
|
|
4
|
-
// 1. package.json shape (workspace name, framework dep)
|
|
5
|
-
// 2. feature.ts is parsable by the canonical-form parser without
|
|
6
|
-
// ParseErrors and contains the Schema-Version-Header
|
|
7
|
-
// 3. featureName extracted by the parser matches what was scaffolded
|
|
8
|
-
// 4. Validation: bad names fail loudly, existing destination refuses
|
|
9
|
-
// to overwrite
|
|
10
|
-
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { parseSourceFile, VERSION_HEADER } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
-
import { Project } from "ts-morph";
|
|
17
|
-
import { scaffoldFeature } from "../scaffold-feature";
|
|
18
|
-
|
|
19
|
-
let workdir: string;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
workdir = mkdtempSync(join(tmpdir(), "kumiko-scaffold-"));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
rmSync(workdir, { recursive: true, force: true });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("scaffoldFeature — output shape", () => {
|
|
30
|
-
test("creates package.json + tsconfig.json + src/feature.ts at the resolved destination", () => {
|
|
31
|
-
const result = scaffoldFeature({
|
|
32
|
-
name: "todoList",
|
|
33
|
-
destination: join(workdir, "todoList"),
|
|
34
|
-
});
|
|
35
|
-
expect(existsSync(result.packageJsonFile)).toBe(true);
|
|
36
|
-
expect(existsSync(result.tsconfigFile)).toBe(true);
|
|
37
|
-
expect(existsSync(result.featureFile)).toBe(true);
|
|
38
|
-
expect(result.featureName).toBe("todoList");
|
|
39
|
-
expect(result.packageName).toBe("@cosmicdrift/kumiko-sample-todo-list");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("tsconfig.json is strict + bundler-resolution + no-emit", () => {
|
|
43
|
-
const result = scaffoldFeature({
|
|
44
|
-
name: "todoList",
|
|
45
|
-
destination: join(workdir, "todoList"),
|
|
46
|
-
});
|
|
47
|
-
const tsconfig = JSON.parse(readFileSync(result.tsconfigFile, "utf8"));
|
|
48
|
-
expect(tsconfig.compilerOptions.strict).toBe(true);
|
|
49
|
-
expect(tsconfig.compilerOptions.noUncheckedIndexedAccess).toBe(true);
|
|
50
|
-
expect(tsconfig.compilerOptions.moduleResolution).toBe("bundler");
|
|
51
|
-
expect(tsconfig.compilerOptions.noEmit).toBe(true);
|
|
52
|
-
expect(tsconfig.include).toEqual(["src/**/*"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("package.json has workspace name + framework dep", () => {
|
|
56
|
-
const result = scaffoldFeature({
|
|
57
|
-
name: "todoList",
|
|
58
|
-
destination: join(workdir, "todoList"),
|
|
59
|
-
});
|
|
60
|
-
const pkg = JSON.parse(readFileSync(result.packageJsonFile, "utf8"));
|
|
61
|
-
expect(pkg.name).toBe("@cosmicdrift/kumiko-sample-todo-list");
|
|
62
|
-
expect(pkg.private).toBe(true);
|
|
63
|
-
expect(pkg.dependencies["@cosmicdrift/kumiko-framework"]).toBe("workspace:*");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("feature.ts starts with the schema-version header", () => {
|
|
67
|
-
const result = scaffoldFeature({
|
|
68
|
-
name: "todoList",
|
|
69
|
-
destination: join(workdir, "todoList"),
|
|
70
|
-
});
|
|
71
|
-
const source = readFileSync(result.featureFile, "utf8");
|
|
72
|
-
expect(source.startsWith(VERSION_HEADER)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("scaffolded feature.ts parses cleanly with no errors", () => {
|
|
76
|
-
const result = scaffoldFeature({
|
|
77
|
-
name: "todoList",
|
|
78
|
-
destination: join(workdir, "todoList"),
|
|
79
|
-
});
|
|
80
|
-
const source = readFileSync(result.featureFile, "utf8");
|
|
81
|
-
const project = new Project({
|
|
82
|
-
skipAddingFilesFromTsConfig: true,
|
|
83
|
-
skipFileDependencyResolution: true,
|
|
84
|
-
useInMemoryFileSystem: true,
|
|
85
|
-
});
|
|
86
|
-
const sf = project.createSourceFile("scaffolded.ts", source);
|
|
87
|
-
const parsed = parseSourceFile(sf);
|
|
88
|
-
expect(parsed.errors).toEqual([]);
|
|
89
|
-
expect(parsed.featureName).toBe("todoList");
|
|
90
|
-
expect(parsed.patterns.length).toBeGreaterThan(0);
|
|
91
|
-
// Starter pattern is an entity, so the user has something to extend.
|
|
92
|
-
expect(parsed.patterns[0]?.kind).toBe("entity");
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("scaffoldFeature — name validation", () => {
|
|
97
|
-
test("rejects empty name", () => {
|
|
98
|
-
expect(() => scaffoldFeature({ name: "", destination: join(workdir, "x") })).toThrow(
|
|
99
|
-
/feature name is required/,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("rejects PascalCase / dashes / numbers-first", () => {
|
|
104
|
-
expect(() => scaffoldFeature({ name: "TodoList", destination: join(workdir, "a") })).toThrow(
|
|
105
|
-
/not a valid feature name/,
|
|
106
|
-
);
|
|
107
|
-
expect(() => scaffoldFeature({ name: "todo-list", destination: join(workdir, "b") })).toThrow(
|
|
108
|
-
/not a valid feature name/,
|
|
109
|
-
);
|
|
110
|
-
expect(() => scaffoldFeature({ name: "1todo", destination: join(workdir, "c") })).toThrow(
|
|
111
|
-
/not a valid feature name/,
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("rejects reserved words", () => {
|
|
116
|
-
expect(() => scaffoldFeature({ name: "delete", destination: join(workdir, "d") })).toThrow(
|
|
117
|
-
/reserved word/,
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe("scaffoldFeature — destination handling", () => {
|
|
123
|
-
test("default destination falls under repoRoot/samples/recipes/<kebab>", () => {
|
|
124
|
-
const result = scaffoldFeature({ name: "todoList", repoRoot: workdir });
|
|
125
|
-
expect(result.destination).toBe(join(workdir, "samples", "recipes", "todo-list"));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("refuses to overwrite an existing destination", () => {
|
|
129
|
-
const dest = join(workdir, "todoList");
|
|
130
|
-
scaffoldFeature({ name: "todoList", destination: dest });
|
|
131
|
-
expect(() => scaffoldFeature({ name: "todoList", destination: dest })).toThrow(
|
|
132
|
-
/already exists/,
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("camelCase name → kebab-case directory + package suffix", () => {
|
|
137
|
-
const result = scaffoldFeature({
|
|
138
|
-
name: "userProfileCustomization",
|
|
139
|
-
destination: join(workdir, "userProfileCustomization"),
|
|
140
|
-
});
|
|
141
|
-
expect(result.packageName).toBe("@cosmicdrift/kumiko-sample-user-profile-customization");
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// Production-Behavior von `kumiko schema apply` (runSchemaApply): der
|
|
2
|
-
// migrate-initContainer ruft das gegen eine frische CNPG-DB. Der riskante,
|
|
3
|
-
// neue Teil gegenüber dem alten per-App-Boilerplate ist der Greenfield-
|
|
4
|
-
// Bootstrap — Infra-Tabellen (event-store + pipeline-state) MÜSSEN vor den
|
|
5
|
-
// App-Migrations idempotent angelegt werden, sonst bricht eine leere DB an
|
|
6
|
-
// `relation "kumiko_events" does not exist`. Dieser Test fährt den echten
|
|
7
|
-
// Pfad gegen eine leere DB + den idempotenten Re-Run.
|
|
8
|
-
|
|
9
|
-
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
10
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import {
|
|
14
|
-
asRawClient,
|
|
15
|
-
buildEntityTable,
|
|
16
|
-
createDbConnection,
|
|
17
|
-
createEventStoreExecutor,
|
|
18
|
-
createTenantDb,
|
|
19
|
-
type DbConnection,
|
|
20
|
-
integer,
|
|
21
|
-
table as pgTable,
|
|
22
|
-
selectMany,
|
|
23
|
-
tableExists,
|
|
24
|
-
uuid,
|
|
25
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
-
import {
|
|
27
|
-
createEntity,
|
|
28
|
-
createTextField,
|
|
29
|
-
defineApply,
|
|
30
|
-
defineFeature,
|
|
31
|
-
type ProjectionDefinition,
|
|
32
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
33
|
-
import {
|
|
34
|
-
createTestDb,
|
|
35
|
-
type TestDb,
|
|
36
|
-
TestUsers,
|
|
37
|
-
unsafeCreateEntityTable,
|
|
38
|
-
unsafePushTables,
|
|
39
|
-
} from "@cosmicdrift/kumiko-framework/stack";
|
|
40
|
-
import { runSchemaApply } from "../schema-apply";
|
|
41
|
-
|
|
42
|
-
let testDb: TestDb;
|
|
43
|
-
let conn: { readonly db: DbConnection; readonly close: () => Promise<void> };
|
|
44
|
-
let appCwd: string;
|
|
45
|
-
let migDir: string;
|
|
46
|
-
const savedDbUrl = process.env["DATABASE_URL"];
|
|
47
|
-
|
|
48
|
-
const APPLY = { features: [], includeBundled: false } as const;
|
|
49
|
-
|
|
50
|
-
beforeAll(async () => {
|
|
51
|
-
const base = process.env["TEST_DATABASE_URL"];
|
|
52
|
-
if (!base) throw new Error("TEST_DATABASE_URL required for schema-apply integration test");
|
|
53
|
-
|
|
54
|
-
testDb = await createTestDb();
|
|
55
|
-
const testUrl = base.replace(/\/[^/]+$/, `/${testDb.dbName}`);
|
|
56
|
-
process.env["DATABASE_URL"] = testUrl;
|
|
57
|
-
conn = createDbConnection(testUrl);
|
|
58
|
-
|
|
59
|
-
// Greenfield erzwingen: createTestDb legt kumiko_events bereits an —
|
|
60
|
-
// wegdroppen, damit runSchemaApply den Infra-Bootstrap echt durchläuft.
|
|
61
|
-
const raw = asRawClient(conn.db);
|
|
62
|
-
await raw.unsafe(`DROP TABLE IF EXISTS "kumiko_events" CASCADE`);
|
|
63
|
-
await raw.unsafe(`DROP TABLE IF EXISTS "kumiko_event_consumers" CASCADE`);
|
|
64
|
-
await raw.unsafe(`DROP TABLE IF EXISTS "kumiko_projections" CASCADE`);
|
|
65
|
-
await raw.unsafe(`DROP TABLE IF EXISTS "_kumiko_migrations" CASCADE`);
|
|
66
|
-
|
|
67
|
-
appCwd = mkdtempSync(join(tmpdir(), "kumiko-schema-apply-"));
|
|
68
|
-
migDir = join(appCwd, "kumiko", "migrations");
|
|
69
|
-
mkdirSync(migDir, { recursive: true });
|
|
70
|
-
writeFileSync(
|
|
71
|
-
join(migDir, "0001_init.sql"),
|
|
72
|
-
`CREATE TABLE "read_thing" ("id" text PRIMARY KEY);`,
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
afterAll(async () => {
|
|
77
|
-
await conn?.close();
|
|
78
|
-
await testDb?.cleanup();
|
|
79
|
-
if (appCwd) rmSync(appCwd, { recursive: true, force: true });
|
|
80
|
-
if (savedDbUrl === undefined) delete process.env["DATABASE_URL"];
|
|
81
|
-
else process.env["DATABASE_URL"] = savedDbUrl;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("runSchemaApply", () => {
|
|
85
|
-
test("Greenfield: leere DB → Infra-Tabellen + App-Migration appliziert → 0", async () => {
|
|
86
|
-
expect(await runSchemaApply({ ...APPLY, appCwd })).toBe(0);
|
|
87
|
-
|
|
88
|
-
expect(await tableExists(conn.db, "public.kumiko_events")).toBe(true);
|
|
89
|
-
expect(await tableExists(conn.db, "public.kumiko_event_consumers")).toBe(true);
|
|
90
|
-
expect(await tableExists(conn.db, "public.kumiko_projections")).toBe(true);
|
|
91
|
-
expect(await tableExists(conn.db, "public.read_thing")).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("Re-Run auf Bestands-DB ist idempotent → 0 (Infra no-op, Migrations skipped)", async () => {
|
|
95
|
-
expect(await runSchemaApply({ ...APPLY, appCwd })).toBe(0);
|
|
96
|
-
expect(await tableExists(conn.db, "public.read_thing")).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("rebuild-Marker für nicht-registrierte Tabelle → kein Crash, 0, aber laut warnen (522/3, #2464)", async () => {
|
|
100
|
-
writeFileSync(
|
|
101
|
-
join(migDir, "0002_more.sql"),
|
|
102
|
-
`CREATE TABLE "read_more" ("id" text PRIMARY KEY);`,
|
|
103
|
-
);
|
|
104
|
-
writeFileSync(
|
|
105
|
-
join(migDir, "0002_more.rebuild.json"),
|
|
106
|
-
JSON.stringify({ version: 1, tables: ["read_more"] }),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// runPendingRebuilds (not the old local helper) owns this warning now —
|
|
110
|
-
// it logs via createFallbackLogger(...).error(...), not console.warn.
|
|
111
|
-
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
112
|
-
expect(await runSchemaApply({ ...APPLY, appCwd })).toBe(0);
|
|
113
|
-
expect(await tableExists(conn.db, "public.read_more")).toBe(true);
|
|
114
|
-
expect(error).toHaveBeenCalledWith(expect.stringContaining("read_more"), expect.anything());
|
|
115
|
-
error.mockRestore();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("failed projection rebuild stays queued and is retried on a later apply with zero new migrations (#2464)", async () => {
|
|
119
|
-
// Isolate from any pending-rebuild rows the earlier tests in this file left.
|
|
120
|
-
await asRawClient(conn.db).unsafe(`DROP TABLE IF EXISTS kumiko_pending_rebuilds`);
|
|
121
|
-
|
|
122
|
-
const groupId = "00000000-0000-4000-8000-0000000000b1";
|
|
123
|
-
let failApply = true;
|
|
124
|
-
|
|
125
|
-
const failItemEntity = createEntity({
|
|
126
|
-
table: "read_apply_fail_items",
|
|
127
|
-
fields: {
|
|
128
|
-
groupId: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
129
|
-
name: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
const failItemTable = buildEntityTable("apply-fail-item", failItemEntity);
|
|
133
|
-
const failCountsTable = pgTable("read_apply_fail_counts", {
|
|
134
|
-
groupId: uuid("group_id").primaryKey(),
|
|
135
|
-
tenantId: uuid("tenant_id").notNull(),
|
|
136
|
-
itemCount: integer("item_count").notNull().default(0),
|
|
137
|
-
});
|
|
138
|
-
const failCountsProjection: ProjectionDefinition = {
|
|
139
|
-
name: "apply-fail-counts",
|
|
140
|
-
source: "apply-fail-item",
|
|
141
|
-
table: failCountsTable,
|
|
142
|
-
apply: {
|
|
143
|
-
"apply-fail-item.created": defineApply<{ groupId: string }>(async (event, tx) => {
|
|
144
|
-
if (failApply) throw new Error("simulated rebuild failure (test)");
|
|
145
|
-
await asRawClient(tx).unsafe(
|
|
146
|
-
`INSERT INTO "read_apply_fail_counts" (group_id, tenant_id, item_count) VALUES ($1::uuid, $2::uuid, 1)
|
|
147
|
-
ON CONFLICT (group_id) DO UPDATE SET item_count = read_apply_fail_counts.item_count + 1`,
|
|
148
|
-
[event.payload.groupId, event.tenantId],
|
|
149
|
-
);
|
|
150
|
-
}),
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
const feature = defineFeature("applyfailtest", (r) => {
|
|
154
|
-
r.entity("apply-fail-item", failItemEntity);
|
|
155
|
-
r.projection(failCountsProjection);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await unsafeCreateEntityTable(conn.db, failItemEntity, "apply-fail-item");
|
|
159
|
-
await unsafePushTables(conn.db, { readApplyFailCounts: failCountsTable });
|
|
160
|
-
|
|
161
|
-
const tdb = createTenantDb(conn.db, TestUsers.admin.tenantId);
|
|
162
|
-
const executor = createEventStoreExecutor(failItemTable, failItemEntity, {
|
|
163
|
-
entityName: "apply-fail-item",
|
|
164
|
-
});
|
|
165
|
-
await executor.create({ groupId, name: "x" }, TestUsers.admin, tdb);
|
|
166
|
-
|
|
167
|
-
writeFileSync(join(migDir, "0003_touch_fail_counts.sql"), "SELECT 1;\n");
|
|
168
|
-
writeFileSync(
|
|
169
|
-
join(migDir, "0003_touch_fail_counts.rebuild.json"),
|
|
170
|
-
JSON.stringify({ version: 1, tables: ["read_apply_fail_counts"] }),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
174
|
-
const firstRun = await runSchemaApply({ features: [feature], includeBundled: false, appCwd });
|
|
175
|
-
error.mockRestore();
|
|
176
|
-
// Fail-loud: a failed rebuild must surface as a non-zero exit, not a
|
|
177
|
-
// silent 0 — the migration itself is now tracked applied, so without a
|
|
178
|
-
// persisted queue this table's rebuild would never be retried again.
|
|
179
|
-
expect(firstRun).toBe(1);
|
|
180
|
-
const [rowAfterFail] = await selectMany(conn.db, failCountsTable, { groupId });
|
|
181
|
-
expect(rowAfterFail).toBeUndefined();
|
|
182
|
-
|
|
183
|
-
// Second apply: no new migrations (0003 is already tracked), yet the
|
|
184
|
-
// queued table must still be retried from kumiko_pending_rebuilds.
|
|
185
|
-
failApply = false;
|
|
186
|
-
const secondRun = await runSchemaApply({ features: [feature], includeBundled: false, appCwd });
|
|
187
|
-
expect(secondRun).toBe(0);
|
|
188
|
-
const [rowAfterRetry] = await selectMany(conn.db, failCountsTable, { groupId });
|
|
189
|
-
expect(rowAfterRetry?.itemCount).toBe(1);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { setupTestStackFromFeatures } from "../setup-test-stack-from-features";
|
|
4
|
-
|
|
5
|
-
const noopFeature = defineFeature("noop-app", () => {});
|
|
6
|
-
|
|
7
|
-
describe("setupTestStackFromFeatures", () => {
|
|
8
|
-
test("includeBundled prepends config/user/tenant/auth", async () => {
|
|
9
|
-
const stack = await setupTestStackFromFeatures([noopFeature], { includeBundled: true });
|
|
10
|
-
try {
|
|
11
|
-
for (const name of ["config", "user", "tenant", "auth-email-password", "noop-app"]) {
|
|
12
|
-
expect(stack.registry.getFeature(name)).toBeDefined();
|
|
13
|
-
}
|
|
14
|
-
} finally {
|
|
15
|
-
await stack.cleanup();
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("config preset wires configResolver in extraContext", async () => {
|
|
20
|
-
const stack = await setupTestStackFromFeatures([noopFeature], {
|
|
21
|
-
includeBundled: true,
|
|
22
|
-
presets: ["config"],
|
|
23
|
-
});
|
|
24
|
-
try {
|
|
25
|
-
expect(stack.registry.getFeature("config")).toBeDefined();
|
|
26
|
-
} finally {
|
|
27
|
-
await stack.cleanup();
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// setupTestStackFromFeatures wires prod's job run-logger by default: a job
|
|
2
|
-
// declared with tenantVisibleFailure records its final failure per tenant, and
|
|
3
|
-
// drainJobs() only rejects after that row is written.
|
|
4
|
-
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
-
import { createJobsFeature, JobQueries } from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
7
|
-
import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
-
import { createTestUser, type TestStack, testTenantId } from "@cosmicdrift/kumiko-framework/stack";
|
|
9
|
-
import * as z from "zod";
|
|
10
|
-
import { setupTestStackFromFeatures } from "../setup-test-stack-from-features";
|
|
11
|
-
|
|
12
|
-
const FAILURE_KEY = "app:errors.generationFailed";
|
|
13
|
-
const PROVIDER_MESSAGE = "provider 429: prompt with customer data rejected";
|
|
14
|
-
|
|
15
|
-
const tenantA = testTenantId(1);
|
|
16
|
-
const tenantB = testTenantId(2);
|
|
17
|
-
const userA = createTestUser({ id: 1, tenantId: tenantA, roles: ["Admin"] });
|
|
18
|
-
const userB = createTestUser({ id: 2, tenantId: tenantB, roles: ["Admin"] });
|
|
19
|
-
|
|
20
|
-
const appFeature = defineFeature("app", (r) => {
|
|
21
|
-
r.writeHandler(
|
|
22
|
-
defineWriteHandler({
|
|
23
|
-
name: "generate",
|
|
24
|
-
description: "Test-only: starts a job that always fails.",
|
|
25
|
-
schema: z.object({ campaignId: z.string() }),
|
|
26
|
-
access: { roles: ["Admin"] },
|
|
27
|
-
handler: async (event) => ({ isSuccess: true as const, data: { ...event.payload } }),
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
r.job(
|
|
31
|
-
"generateTexts",
|
|
32
|
-
{
|
|
33
|
-
trigger: { on: "app:write:generate" },
|
|
34
|
-
retries: 0,
|
|
35
|
-
tenantVisibleFailure: { messageKey: FAILURE_KEY, subjectFields: ["campaignId"] },
|
|
36
|
-
},
|
|
37
|
-
async () => {
|
|
38
|
-
throw new Error(PROVIDER_MESSAGE);
|
|
39
|
-
},
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
type FailureRow = { readonly jobName: string; readonly messageKey: string };
|
|
44
|
-
|
|
45
|
-
let stack: TestStack;
|
|
46
|
-
|
|
47
|
-
beforeAll(async () => {
|
|
48
|
-
stack = await setupTestStackFromFeatures([appFeature, createJobsFeature()], {
|
|
49
|
-
jobs: { consumerLane: "worker" },
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
afterAll(async () => {
|
|
54
|
-
await stack.cleanup();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("setupTestStackFromFeatures({ jobs }) run-logger default", () => {
|
|
58
|
-
test("a failed tenant-visible job is readable by its tenant only, once drainJobs() rejects", async () => {
|
|
59
|
-
await stack.http.writeOk("app:write:generate", { campaignId: "campaign-1" }, userA);
|
|
60
|
-
|
|
61
|
-
await expect(stack.drainJobs()).rejects.toThrow(/app:job:generate-texts/);
|
|
62
|
-
|
|
63
|
-
const own = await stack.http.queryOk<{ rows: FailureRow[] }>(JobQueries.failures, {}, userA);
|
|
64
|
-
expect(own.rows).toHaveLength(1);
|
|
65
|
-
expect(own.rows[0]?.jobName).toBe("app:job:generate-texts");
|
|
66
|
-
expect(own.rows[0]?.messageKey).toBe(FAILURE_KEY);
|
|
67
|
-
expect(JSON.stringify(own)).not.toContain(PROVIDER_MESSAGE);
|
|
68
|
-
|
|
69
|
-
const other = await stack.http.queryOk<{ rows: FailureRow[] }>(JobQueries.failures, {}, userB);
|
|
70
|
-
expect(other.rows).toHaveLength(0);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
// DX-3.1 — walkthrough-snapshot-test. Reproduces the 3-command path from
|
|
2
|
-
// docs.kumiko.rocks/en/walkthrough/ in-process and asserts what the walkthrough
|
|
3
|
-
// claims. Catches drift in scaffoldApp + scaffoldAppFeature against the
|
|
4
|
-
// docs without an actual `bunx … && bun install && bun run boot` CI run.
|
|
5
|
-
//
|
|
6
|
-
// What this test pins:
|
|
7
|
-
// - scaffoldApp produces the files the walkthrough lists
|
|
8
|
-
// - scaffoldAppFeature scaffolds + auto-mounts (the diff-block shown)
|
|
9
|
-
// - composeFeatures(includeBundled:true) yields the exact feature-count
|
|
10
|
-
// the walkthrough advertises in "Expected output"
|
|
11
|
-
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
17
|
-
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
18
|
-
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
19
|
-
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
20
|
-
import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
-
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
22
|
-
import { scaffoldApp } from "../scaffold-app";
|
|
23
|
-
import { scaffoldAppFeature } from "../scaffold-app-feature";
|
|
24
|
-
|
|
25
|
-
describe("walkthrough — DX-3.1 snapshot", () => {
|
|
26
|
-
let tmp: string;
|
|
27
|
-
let appRoot: string;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
tmp = mkdtempSync(join(tmpdir(), "walkthrough-"));
|
|
31
|
-
appRoot = join(tmp, "my-notes");
|
|
32
|
-
});
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("Step 1 (kumiko new app) — produces walkthrough's scaffold files", async () => {
|
|
38
|
-
const result = await scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
39
|
-
expect(result.files).toContain("package.json");
|
|
40
|
-
expect(result.files).toContain("kumiko/schema.ts");
|
|
41
|
-
expect(result.files).toContain("bin/kumiko.ts");
|
|
42
|
-
expect(result.files).toContain("deploy/Dockerfile");
|
|
43
|
-
expect(result.files).toContain("kumiko/migrations/0001_init.sql");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("Step 2 (kumiko add feature) — auto-mounts + walkthrough diff matches", async () => {
|
|
47
|
-
await scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
48
|
-
const result = scaffoldAppFeature({ name: "notes", appRoot });
|
|
49
|
-
expect(result.autoMounted).toBe(true);
|
|
50
|
-
|
|
51
|
-
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
52
|
-
// Walkthrough's diff-block claims:
|
|
53
|
-
// + import { notesFeature } from "./features/notes";
|
|
54
|
-
// + notesFeature (in APP_FEATURES)
|
|
55
|
-
expect(runConfig).toContain(`import { notesFeature } from "./features/notes";`);
|
|
56
|
-
expect(runConfig).toContain("notesFeature");
|
|
57
|
-
// Foundation still mounted (createSecretsFeature + createSessionsFeature).
|
|
58
|
-
expect(runConfig).toContain("createSecretsFeature()");
|
|
59
|
-
expect(runConfig).toContain("createSessionsFeature()");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("Step 3 (boot validation) — scaffolded run-config matches walkthrough's APP_FEATURES claim", async () => {
|
|
63
|
-
await scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
64
|
-
scaffoldAppFeature({ name: "notes", appRoot });
|
|
65
|
-
|
|
66
|
-
// Text-assert: scaffolded run-config.ts contains exactly the 3 features
|
|
67
|
-
// the walkthrough's diff-block shows (secrets + sessions + notesFeature).
|
|
68
|
-
// Dynamic-import would fail because /tmp can't resolve @cosmicdrift/*
|
|
69
|
-
// workspace symlinks — instead we reproduce the equivalent APP_FEATURES
|
|
70
|
-
// array in-process below.
|
|
71
|
-
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
72
|
-
expect(runConfig).toContain("createSecretsFeature()");
|
|
73
|
-
expect(runConfig).toContain("createSessionsFeature()");
|
|
74
|
-
expect(runConfig).toContain("notesFeature");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("Step 3 (composeFeatures) — 3 explicit + 4 auto-mounted = 7 features", () => {
|
|
78
|
-
// Reproduces the scaffolded APP_FEATURES in-process. notesFeature gets
|
|
79
|
-
// a dummy defineFeature here — the scaffold-side of "notesFeature"
|
|
80
|
-
// (file-content) is pinned in test 2; this test pins the runtime-side
|
|
81
|
-
// (composeFeatures auto-prepend behaviour the walkthrough claims).
|
|
82
|
-
//
|
|
83
|
-
// sessions now requires auth-foundation (#1370/#1371), which itself
|
|
84
|
-
// needs a tokenVerifier provider mounted (#1368) — PAT here, mirroring
|
|
85
|
-
// the scaffold picker's real-world default. Not part of the walkthrough's
|
|
86
|
-
// advertised 7-feature count (that count is about includeBundled's
|
|
87
|
-
// auto-mount, unrelated to this coupling), so both are added on top.
|
|
88
|
-
const notesFeature = defineFeature("notes", () => {});
|
|
89
|
-
const APP_FEATURES = [
|
|
90
|
-
createSecretsFeature(),
|
|
91
|
-
authFoundationFeature,
|
|
92
|
-
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
93
|
-
createSessionsFeature(),
|
|
94
|
-
notesFeature,
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
|
|
98
|
-
// 5 explicit + 4 auto-mounted bundled = 9 total features.
|
|
99
|
-
expect(composed.length).toBe(9);
|
|
100
|
-
|
|
101
|
-
const composedNames = composed.map((f) => f.name).sort();
|
|
102
|
-
expect(composedNames).toEqual([
|
|
103
|
-
"auth-email-password",
|
|
104
|
-
"auth-foundation",
|
|
105
|
-
"config",
|
|
106
|
-
"notes",
|
|
107
|
-
"personal-access-tokens",
|
|
108
|
-
"secrets",
|
|
109
|
-
"sessions",
|
|
110
|
-
"tenant",
|
|
111
|
-
"user",
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
// validateBoot must pass (no missing-requires, no schema-errors).
|
|
115
|
-
expect(() => validateBoot(composed)).not.toThrow();
|
|
116
|
-
// Registry must contain all 9 features.
|
|
117
|
-
const registry = createRegistry(composed);
|
|
118
|
-
expect(registry.features.size).toBe(9);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("bin/main.ts contains the auth.admin stub the walkthrough relies on", async () => {
|
|
122
|
-
await scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
123
|
-
const main = readFileSync(join(appRoot, "bin/main.ts"), "utf-8");
|
|
124
|
-
// composeFeatures(includeBundled:true)-trigger is `auth: { admin: { … } }`.
|
|
125
|
-
// Walkthrough explicitly says this is what auto-mounts the 4 bundled features.
|
|
126
|
-
expect(main).toContain("auth: {");
|
|
127
|
-
expect(main).toContain("admin: {");
|
|
128
|
-
expect(main).toContain("memberships:");
|
|
129
|
-
expect(main).toContain("runProdApp");
|
|
130
|
-
// Prod writer stays untouched — the verify flow must run there (#1687).
|
|
131
|
-
expect(main).not.toContain("emailVerified");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("bin/dev.ts seeds the admin as emailVerified: true so login doesn't 422", async () => {
|
|
135
|
-
await scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
136
|
-
const dev = readFileSync(join(appRoot, "bin/dev.ts"), "utf-8");
|
|
137
|
-
expect(dev).toContain("admin: {");
|
|
138
|
-
expect(dev).toContain("emailVerified: true,");
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { renderWelcomeBanner } from "../welcome-banner";
|
|
3
|
-
|
|
4
|
-
describe("renderWelcomeBanner", () => {
|
|
5
|
-
test("includes URL + admin login + features dir + docs link", () => {
|
|
6
|
-
const banner = renderWelcomeBanner({
|
|
7
|
-
url: "http://localhost:3000",
|
|
8
|
-
admin: { email: "admin@demo.test", password: "changeme" },
|
|
9
|
-
});
|
|
10
|
-
expect(banner).toContain("http://localhost:3000");
|
|
11
|
-
expect(banner).toContain("admin@demo.test");
|
|
12
|
-
expect(banner).toContain("changeme");
|
|
13
|
-
expect(banner).toContain("src/features/");
|
|
14
|
-
expect(banner).toContain("docs.kumiko.rocks");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("box rows are aligned (all the same printable width)", () => {
|
|
18
|
-
const banner = renderWelcomeBanner({
|
|
19
|
-
url: "http://localhost:3000",
|
|
20
|
-
admin: { email: "a@b.c", password: "x" },
|
|
21
|
-
});
|
|
22
|
-
const rows = banner.split("\n");
|
|
23
|
-
const widths = rows.map((r) => [...r].length);
|
|
24
|
-
expect(new Set(widths).size).toBe(1);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("admin is optional (no-auth dev runs still get a banner)", () => {
|
|
28
|
-
const banner = renderWelcomeBanner({ url: "http://localhost:3000" });
|
|
29
|
-
expect(banner).toContain("http://localhost:3000");
|
|
30
|
-
expect(banner).not.toContain("Login als");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("featuresDir + docsUrl overridable", () => {
|
|
34
|
-
const banner = renderWelcomeBanner({
|
|
35
|
-
url: "http://localhost:3000",
|
|
36
|
-
featuresDir: "app/modules/",
|
|
37
|
-
docsUrl: "https://internal.example.com/dev",
|
|
38
|
-
});
|
|
39
|
-
expect(banner).toContain("app/modules/");
|
|
40
|
-
expect(banner).toContain("internal.example.com/dev");
|
|
41
|
-
});
|
|
42
|
-
});
|