@cosmicdrift/kumiko-dev-server 0.105.1 → 0.105.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.105.1",
3
+ "version": "0.105.2",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -50,8 +50,8 @@
50
50
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@cosmicdrift/kumiko-bundled-features": "0.105.1",
54
- "@cosmicdrift/kumiko-framework": "0.105.1",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.105.2",
54
+ "@cosmicdrift/kumiko-framework": "0.105.2",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -7,7 +7,7 @@
7
7
  // aber die Handler fehlen → POST /api/auth/request-password-reset
8
8
  // dispatched ins Leere → 500.
9
9
 
10
- import { describe, expect, test } from "bun:test";
10
+ import { describe, expect, spyOn, test } from "bun:test";
11
11
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { composeFeatures } from "../compose-features";
13
13
 
@@ -104,9 +104,14 @@ describe("composeFeatures", () => {
104
104
  // copy via includeBundled:true, and createRegistry throws "Duplicate
105
105
  // feature: auth-email-password". The dedupe path keeps the bundled
106
106
  // instance (it carries authOptions wiring) and drops the app stub.
107
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
107
108
  const features = composeFeatures([pickerAuthDupe, noopFeature], {
108
109
  includeBundled: true,
109
110
  });
111
+ // The warn is part of the fix contract (changeset: "warn so the user can
112
+ // remove the line") — without this assertion the warn is removable with no RED.
113
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("auth-email-password"));
114
+ warnSpy.mockRestore();
110
115
  expect(features.map((f) => f.name)).toEqual([
111
116
  "config",
112
117
  "user",
@@ -5,13 +5,19 @@ import {
5
5
  createConfigFeature,
6
6
  createConfigResolver,
7
7
  } from "@cosmicdrift/kumiko-bundled-features/config";
8
- import { access, createTenantConfig, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
8
+ import {
9
+ access,
10
+ createTenantConfig,
11
+ defineFeature,
12
+ defineQueryHandler,
13
+ } from "@cosmicdrift/kumiko-framework/engine";
9
14
  import {
10
15
  setupTestStack,
11
16
  type TestStack,
12
17
  TestUsers,
13
18
  unsafePushTables,
14
19
  } from "@cosmicdrift/kumiko-framework/stack";
20
+ import { z } from "zod";
15
21
  import { mergeConfigResolverDefault } from "../run-dev-app";
16
22
 
17
23
  // Pins runDevApp's ENV→config-app-override wiring: mergeConfigResolverDefault
@@ -22,6 +28,18 @@ import { mergeConfigResolverDefault } from "../run-dev-app";
22
28
 
23
29
  const PAGE_SIZE = "devcfg:config:page-size";
24
30
 
31
+ // Reads ctx.config directly — the exact consumption seam mergeConfigResolverDefault
32
+ // wires the dispatcher through. Every other test in this file inspects the returned
33
+ // {configResolver, _configAccessorFactory} object without ever dispatching a handler
34
+ // that actually reads ctx.config; a "Boot writes field A, dispatcher expects field B"
35
+ // drift (the original #359-adjacent bug class) would be invisible to all of them.
36
+ const readPageSizeQuery = defineQueryHandler({
37
+ name: "read-page-size",
38
+ schema: z.object({}),
39
+ access: { openToAll: true },
40
+ handler: async (_query, ctx) => ({ pageSize: await ctx.config?.(PAGE_SIZE) }),
41
+ });
42
+
25
43
  const devcfgFeature = defineFeature("devcfg", (r) => {
26
44
  r.requires("config");
27
45
  r.config({
@@ -34,6 +52,7 @@ const devcfgFeature = defineFeature("devcfg", (r) => {
34
52
  }),
35
53
  },
36
54
  });
55
+ r.queryHandler(readPageSizeQuery);
37
56
  });
38
57
 
39
58
  // ctx=undefined → object form, configResolver is the only key (test boundary).
@@ -128,4 +147,19 @@ describe("runDevApp configResolver-default — ENV→app-override bridge", () =>
128
147
  // was built from the caller's resolver, exactly like money-horse's "s3-env".
129
148
  expect(await accessor(PAGE_SIZE)).toBe("99");
130
149
  });
150
+
151
+ test("a real dispatch through stack.http resolves ctx.config end-to-end (not the factory called directly)", async () => {
152
+ // Every test above calls mergeConfigResolverDefault()/_configAccessorFactory
153
+ // directly — none go through an actual dispatched request. The original bug
154
+ // this feature guards against ("boot writes configResolver, dispatcher reads
155
+ // a different field") only manifests on a REAL dispatch, where the
156
+ // dispatcher's own wiring (not test code) builds ctx.config from
157
+ // _configAccessorFactory. This is that missing end-to-end proof.
158
+ const res = await stack.http.queryOk<{ pageSize: number | undefined }>(
159
+ "devcfg:query:read-page-size",
160
+ {},
161
+ TestUsers.systemAdmin,
162
+ );
163
+ expect(res.pageSize).toBe(10); // keyDef.default — no env override wired on this stack
164
+ });
131
165
  });
@@ -6,7 +6,18 @@
6
6
  // path would silently drop the fail-closed guard and dev/prod would diverge.
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
9
+ import {
10
+ requireTextContent,
11
+ type TextContentApi,
12
+ } from "@cosmicdrift/kumiko-bundled-features/text-content";
13
+ import {
14
+ createEntity,
15
+ createTextField,
16
+ defineFeature,
17
+ defineQueryHandler,
18
+ } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
20
+ import { z } from "zod";
10
21
  import type { KumikoServerHandle } from "../create-kumiko-server";
11
22
  import { runDevApp } from "../run-dev-app";
12
23
 
@@ -37,6 +48,24 @@ afterEach(async () => {
37
48
  else process.env["JWT_SECRET"] = savedJwtSecret;
38
49
  });
39
50
 
51
+ describe("runDevApp — auth.mail fail-fast on missing JWT_SECRET", () => {
52
+ test("options.auth set but JWT_SECRET missing → throws, no dev-fallback", async () => {
53
+ // beforeEach sets JWT_SECRET unconditionally for every OTHER test in this
54
+ // file — none of them ever exercise the missing-secret path, so it's
55
+ // unclear whether the fail-closed guard (requireEnv, not the old dev-
56
+ // fallback) actually fires or was silently dropped. Pass a custom
57
+ // envSource without JWT_SECRET instead of touching process.env.
58
+ const { JWT_SECRET: _drop, ...envWithoutJwtSecret } = process.env;
59
+ await expect(
60
+ runDevApp({
61
+ features: [validFeature()],
62
+ auth: { admin: ADMIN, cookieDomain: "example.eu", allowedOrigins: ["https://example.eu"] },
63
+ envSource: envWithoutJwtSecret,
64
+ }),
65
+ ).rejects.toThrow(/JWT_SECRET/);
66
+ });
67
+ });
68
+
40
69
  describe("runDevApp — auth allowedOrigins forwarding (#399/1)", () => {
41
70
  test("cookieDomain without allowedOrigins fails closed — guard is wired through runDevApp", async () => {
42
71
  await expect(
@@ -80,3 +109,48 @@ describe("runDevApp — auth allowedOrigins forwarding (#399/1)", () => {
80
109
  expect(handle).toBeDefined();
81
110
  });
82
111
  });
112
+
113
+ describe("runDevApp — extraContext merge order: app values win over boot defaults (707/1)", () => {
114
+ test("a caller-supplied extraContext.textContent wins over the auto-wired boot default", async () => {
115
+ // runDevApp's extraContext factory does `{ ...boot, ...base }` — boot's
116
+ // auto-wired textContent must lose to a caller-supplied override, exactly
117
+ // the "app values win" parity claim this PR made against runProdApp. Every
118
+ // OTHER test in this file only checks that runDevApp boots without
119
+ // throwing; none dispatch a request that actually reads the merged value.
120
+ const sentinel: TextContentApi = {
121
+ getBlock: async () => ({
122
+ slug: "sentinel",
123
+ lang: "en",
124
+ title: "from caller extraContext",
125
+ body: null,
126
+ updatedAt: new Date(0),
127
+ }),
128
+ };
129
+ const readBlockQuery = defineQueryHandler({
130
+ name: "read-block",
131
+ schema: z.object({}),
132
+ access: { openToAll: true },
133
+ handler: async (_query, ctx) => {
134
+ const api = requireTextContent(ctx, "textcheck:query:read-block");
135
+ return api.getBlock({ tenantId: TestUsers.systemAdmin.tenantId, slug: "x", lang: "en" });
136
+ },
137
+ });
138
+ const textcheckFeature = defineFeature("textcheck", (r) => {
139
+ r.queryHandler(readBlockQuery);
140
+ });
141
+
142
+ handle = await runDevApp({
143
+ features: [validFeature(), textcheckFeature],
144
+ port: 0,
145
+ extraContext: { textContent: sentinel },
146
+ });
147
+
148
+ const res = await handle.stack.http.queryOk<{ title: string } | null>(
149
+ "textcheck:query:read-block",
150
+ {},
151
+ TestUsers.systemAdmin,
152
+ );
153
+
154
+ expect(res?.title).toBe("from caller extraContext");
155
+ });
156
+ });
@@ -1,4 +1,8 @@
1
1
  import { describe, expect, it } from "bun:test";
2
+ import {
3
+ createSessionsFeature,
4
+ SESSIONS_FEATURE,
5
+ } from "@cosmicdrift/kumiko-bundled-features/sessions";
2
6
  import { resolveProdSessionsConfig, shouldWireProdSessions } from "../session-wiring";
3
7
 
4
8
  describe("shouldWireProdSessions — secure-by-default with opt-out (KF-1)", () => {
@@ -25,6 +29,16 @@ describe("shouldWireProdSessions — secure-by-default with opt-out (KF-1)", ()
25
29
  });
26
30
  });
27
31
 
32
+ describe("SESSIONS_FEATURE constant matches the real feature name", () => {
33
+ it("createSessionsFeature()'s name equals SESSIONS_FEATURE", () => {
34
+ // shouldWireProdSessions's own arm only tests the pure boolean helper —
35
+ // the actual run-prod-app.ts integration seam
36
+ // (`features.some((f) => f.name === SESSIONS_FEATURE)`) drifts silently
37
+ // if the feature is ever renamed without updating this constant.
38
+ expect(createSessionsFeature().name).toBe(SESSIONS_FEATURE);
39
+ });
40
+ });
41
+
28
42
  describe("resolveProdSessionsConfig", () => {
29
43
  it("passes a config object through", () => {
30
44
  expect(resolveProdSessionsConfig({ expiresInMs: 5000 })).toEqual({ expiresInMs: 5000 });