@cosmicdrift/kumiko-server-runtime 0.329.0 → 0.330.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/boot/apply-boot-seeds.d.ts +8 -0
- package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
- package/dist/boot/boot-crypto.d.ts +9 -0
- package/dist/boot/boot-crypto.js +39 -0
- package/dist/boot/job-run-logger.d.ts +19 -0
- package/dist/boot/job-run-logger.js +44 -0
- package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
- package/dist/boot/pii-event-backfill-on-boot.js +25 -0
- package/dist/build-prod-bundle.d.ts +103 -0
- package/dist/build-prod-bundle.js +751 -0
- package/dist/bun-serve-options.d.ts +32 -0
- package/dist/bun-serve-options.js +49 -0
- package/dist/compose-features.d.ts +31 -0
- package/dist/compose-features.js +124 -0
- package/dist/extra-routes-deps.d.ts +30 -0
- package/dist/extra-routes-deps.js +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/pii-boot-gate.d.ts +13 -0
- package/dist/pii-boot-gate.js +46 -0
- package/dist/renderer-web-fonts.d.ts +9 -0
- package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
- package/dist/resolve-tailwind-cli.d.ts +14 -0
- package/dist/resolve-tailwind-cli.js +36 -0
- package/dist/run-prod-app-boot-context.d.ts +43 -0
- package/dist/run-prod-app-boot-context.js +198 -0
- package/dist/run-prod-app-static-files.d.ts +32 -0
- package/dist/run-prod-app-static-files.js +280 -0
- package/dist/run-prod-app.d.ts +471 -0
- package/dist/run-prod-app.js +741 -0
- package/dist/run-worker-app.d.ts +92 -0
- package/dist/run-worker-app.js +220 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/security-headers.js +49 -0
- package/dist/session-boot-gate.d.ts +11 -0
- package/dist/session-boot-gate.js +22 -0
- package/dist/session-wiring.d.ts +11 -0
- package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
- package/dist/try-hono-first.d.ts +24 -0
- package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
- package/package.json +102 -36
- package/src/changes.json +6 -0
- package/src/__tests__/boot-extra-context.test.ts +0 -163
- package/src/__tests__/boot-probe-fixture.ts +0 -64
- package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
- package/src/__tests__/build-prod-bundle.test.ts +0 -724
- 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 -609
- package/src/__tests__/compose-features.test.ts +0 -206
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/pii-boot-gate.test.ts +0 -112
- 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__/renderer-web-theme-plugin.integration.test.ts +0 -77
- package/src/__tests__/require-env.test.ts +0 -34
- package/src/__tests__/resolve-auth-mail.test.ts +0 -144
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
- package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
- package/src/__tests__/run-prod-app-spec.test.ts +0 -167
- package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
- package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
- package/src/__tests__/run-worker-app-boot.test.ts +0 -183
- package/src/__tests__/run-worker-app.integration.test.ts +0 -218
- package/src/__tests__/security-headers.test.ts +0 -88
- package/src/__tests__/session-boot-gate.test.ts +0 -70
- package/src/__tests__/session-wiring.test.ts +0 -16
- package/src/__tests__/try-hono-first.test.ts +0 -108
- package/src/__tests__/websocket-route.integration.test.ts +0 -695
- package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/boot-crypto.ts +0 -67
- package/src/boot/job-run-logger.ts +0 -63
- package/src/boot/pii-event-backfill-on-boot.ts +0 -43
- package/src/build-prod-bundle.ts +0 -996
- package/src/bun-serve-options.ts +0 -110
- package/src/compose-features.ts +0 -197
- package/src/extra-routes-deps.ts +0 -38
- package/src/index.ts +0 -27
- package/src/pii-boot-gate.ts +0 -80
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -319
- package/src/run-prod-app-static-files.ts +0 -359
- package/src/run-prod-app.ts +0 -1420
- package/src/run-worker-app.ts +0 -366
- package/src/security-headers.ts +0 -67
- package/src/session-boot-gate.ts +0 -34
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// Unit-Tests für composeFeatures.authOptions — pinst dass die
|
|
2
|
-
// passwordReset / emailVerification options an
|
|
3
|
-
// createAuthEmailPasswordFeature durchgereicht werden, sodass die
|
|
4
|
-
// request- und confirm-Handler im resultierenden Feature registriert
|
|
5
|
-
// sind. Bug-Pattern: ohne diesen Wiring würde runProdApp.options.auth.
|
|
6
|
-
// passwordReset = {sendResetEmail, appResetUrl} die routes mounten,
|
|
7
|
-
// aber die Handler fehlen → POST /api/auth/request-password-reset
|
|
8
|
-
// dispatched ins Leere → 500.
|
|
9
|
-
|
|
10
|
-
import { describe, expect, spyOn, test } from "bun:test";
|
|
11
|
-
import { securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
|
|
12
|
-
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
13
|
-
import { defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
-
import { composeFeatures } from "../compose-features";
|
|
15
|
-
|
|
16
|
-
const noopFeature = defineFeature("noop-app", () => {});
|
|
17
|
-
|
|
18
|
-
// Mirrors what the create-kumiko-app picker hands back when the user
|
|
19
|
-
// ticks an auto-mounted feature: a stub with the same name as a bundled
|
|
20
|
-
// one. The dedupe path drops it and warns.
|
|
21
|
-
const pickerAuthDupe = defineFeature("auth-email-password", () => {});
|
|
22
|
-
|
|
23
|
-
const HMAC_SECRET = "test-secret-with-at-least-32-bytes-aaa";
|
|
24
|
-
|
|
25
|
-
describe("composeFeatures", () => {
|
|
26
|
-
test("includeBundled=false → nur App-Features", () => {
|
|
27
|
-
const features = composeFeatures([noopFeature], { includeBundled: false });
|
|
28
|
-
expect(features.map((f) => f.name)).toEqual(["noop-app"]);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("includeBundled=true → 4 bundled Features davor", () => {
|
|
32
|
-
const features = composeFeatures([noopFeature], { includeBundled: true });
|
|
33
|
-
expect(features.map((f) => f.name)).toEqual([
|
|
34
|
-
"config",
|
|
35
|
-
"user",
|
|
36
|
-
"tenant",
|
|
37
|
-
"auth-email-password",
|
|
38
|
-
"noop-app",
|
|
39
|
-
]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("authOptions.passwordReset → request-password-reset + reset-password handlers registriert", () => {
|
|
43
|
-
const features = composeFeatures([noopFeature], {
|
|
44
|
-
includeBundled: true,
|
|
45
|
-
authOptions: { passwordReset: { hmacSecret: HMAC_SECRET, appUrl: "https://app/reset" } },
|
|
46
|
-
});
|
|
47
|
-
const auth = features.find((f) => f.name === "auth-email-password");
|
|
48
|
-
expect(auth).toBeDefined();
|
|
49
|
-
if (!auth) return;
|
|
50
|
-
const handlerNames = Array.from(Object.keys(auth.writeHandlers));
|
|
51
|
-
expect(handlerNames).toContain("request-password-reset");
|
|
52
|
-
expect(handlerNames).toContain("reset-password");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("authOptions.emailVerification → request-email-verification + verify-email handlers registriert", () => {
|
|
56
|
-
const features = composeFeatures([noopFeature], {
|
|
57
|
-
includeBundled: true,
|
|
58
|
-
authOptions: { emailVerification: { hmacSecret: HMAC_SECRET, appUrl: "https://app/verify" } },
|
|
59
|
-
});
|
|
60
|
-
const auth = features.find((f) => f.name === "auth-email-password");
|
|
61
|
-
expect(auth).toBeDefined();
|
|
62
|
-
if (!auth) return;
|
|
63
|
-
const handlerNames = Array.from(Object.keys(auth.writeHandlers));
|
|
64
|
-
expect(handlerNames).toContain("request-email-verification");
|
|
65
|
-
expect(handlerNames).toContain("verify-email");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("authOptions.signup → signup-request + signup-confirm handlers registriert", () => {
|
|
69
|
-
const features = composeFeatures([noopFeature], {
|
|
70
|
-
includeBundled: true,
|
|
71
|
-
authOptions: { signup: { appUrl: "https://app/signup/complete" } },
|
|
72
|
-
});
|
|
73
|
-
const auth = features.find((f) => f.name === "auth-email-password");
|
|
74
|
-
expect(auth).toBeDefined();
|
|
75
|
-
if (!auth) return;
|
|
76
|
-
const handlerNames = Array.from(Object.keys(auth.writeHandlers));
|
|
77
|
-
expect(handlerNames).toContain("signup-request");
|
|
78
|
-
expect(handlerNames).toContain("signup-confirm");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Regression: signup-request no-ops silently (always-200 anti-enumeration
|
|
82
|
-
// contract) unless "auth-self-registration" is mounted. composeFeatures
|
|
83
|
-
// must bundle it alongside authOptions.signup, not leave apps using the
|
|
84
|
-
// includeBundled convenience path to mount it by hand.
|
|
85
|
-
test("authOptions.signup → auth-self-registration mounted, default ON", () => {
|
|
86
|
-
const features = composeFeatures([noopFeature], {
|
|
87
|
-
includeBundled: true,
|
|
88
|
-
authOptions: { signup: { appUrl: "https://app/signup/complete" } },
|
|
89
|
-
});
|
|
90
|
-
const toggle = features.find((f) => f.name === "auth-self-registration");
|
|
91
|
-
expect(toggle).toBeDefined();
|
|
92
|
-
// The security-relevant part isn't that the feature is mounted, it's
|
|
93
|
-
// that self-signup defaults ON — a flip to `default: false` upstream
|
|
94
|
-
// would silently no-op signup (always-200 anti-enumeration masks it)
|
|
95
|
-
// while this assertion alone (toBeDefined) stayed green.
|
|
96
|
-
expect(toggle?.toggleableDefault).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("no authOptions.signup → auth-self-registration NOT mounted", () => {
|
|
100
|
-
const features = composeFeatures([noopFeature], { includeBundled: true });
|
|
101
|
-
expect(features.map((f) => f.name)).not.toContain("auth-self-registration");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("authOptions.signup + app also mounts its own auth-self-registration stub → deduped to exactly one", () => {
|
|
105
|
-
const pickerSelfRegDupe = defineFeature("auth-self-registration", () => {});
|
|
106
|
-
const features = composeFeatures([noopFeature, pickerSelfRegDupe], {
|
|
107
|
-
includeBundled: true,
|
|
108
|
-
authOptions: { signup: { appUrl: "https://app/signup/complete" } },
|
|
109
|
-
});
|
|
110
|
-
const names = features.map((f) => f.name).filter((n) => n === "auth-self-registration");
|
|
111
|
-
expect(names).toEqual(["auth-self-registration"]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("OHNE authOptions → KEINE reset/verify-handlers (anti-default-deploy-bug)", () => {
|
|
115
|
-
// Genau der Bug der vom Review-Agent gefangen wurde: composeFeatures
|
|
116
|
-
// ohne authOptions registriert die handler nicht. Wenn jemand das
|
|
117
|
-
// versehentlich vergisst und nur die routes (auth-routes-config)
|
|
118
|
-
// wired, schlagen die requests auf prod fehl. Der Test pinst dass
|
|
119
|
-
// dieser default-deny-Pfad bewusst leer ist.
|
|
120
|
-
const features = composeFeatures([noopFeature], { includeBundled: true });
|
|
121
|
-
const auth = features.find((f) => f.name === "auth-email-password");
|
|
122
|
-
expect(auth).toBeDefined();
|
|
123
|
-
if (!auth) return;
|
|
124
|
-
const handlerNames = Array.from(Object.keys(auth.writeHandlers));
|
|
125
|
-
expect(handlerNames).not.toContain("request-password-reset");
|
|
126
|
-
expect(handlerNames).not.toContain("reset-password");
|
|
127
|
-
expect(handlerNames).not.toContain("request-email-verification");
|
|
128
|
-
expect(handlerNames).not.toContain("verify-email");
|
|
129
|
-
// Die regulären auth-handlers (login/logout/change-password) MÜSSEN
|
|
130
|
-
// aber immer da sein — das ist der core-flow.
|
|
131
|
-
expect(handlerNames).toContain("login");
|
|
132
|
-
expect(handlerNames).toContain("logout");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// fw#2223 regression: without this wiring, includeBundled apps that DO
|
|
136
|
-
// configure authOptions.invite still got /members with no invite button —
|
|
137
|
-
// the screen is bound to auth-email-password's invite-create handler,
|
|
138
|
-
// which only exists when opts.invite is set (feature.ts:238-239).
|
|
139
|
-
test("authOptions.invite → tenant bekommt invite-Screen + Toolbar-Action; ohne invite bootet sauber ohne beides", () => {
|
|
140
|
-
const withInvite = composeFeatures([noopFeature], {
|
|
141
|
-
includeBundled: true,
|
|
142
|
-
authOptions: { invite: { appUrl: "https://app/invite" } },
|
|
143
|
-
});
|
|
144
|
-
const tenantWith = withInvite.find((f) => f.name === "tenant");
|
|
145
|
-
expect(tenantWith).toBeDefined();
|
|
146
|
-
if (!tenantWith) return;
|
|
147
|
-
const membersScreenWith = tenantWith.screens["members"];
|
|
148
|
-
if (membersScreenWith?.type !== "projectionList") {
|
|
149
|
-
throw new Error("expected members screen to be projectionList");
|
|
150
|
-
}
|
|
151
|
-
expect(membersScreenWith.toolbarActions?.some((a) => a.id === "invite")).toBe(true);
|
|
152
|
-
expect(tenantWith.screens["invite-create"]).toBeDefined();
|
|
153
|
-
|
|
154
|
-
const withoutInvite = composeFeatures([noopFeature], { includeBundled: true });
|
|
155
|
-
const tenantWithout = withoutInvite.find((f) => f.name === "tenant");
|
|
156
|
-
expect(tenantWithout).toBeDefined();
|
|
157
|
-
if (!tenantWithout) return;
|
|
158
|
-
const membersScreenWithout = tenantWithout.screens["members"];
|
|
159
|
-
if (membersScreenWithout?.type !== "projectionList") {
|
|
160
|
-
throw new Error("expected members screen to be projectionList");
|
|
161
|
-
}
|
|
162
|
-
expect(membersScreenWithout.toolbarActions ?? []).toHaveLength(0);
|
|
163
|
-
expect(tenantWithout.screens["invite-create"]).toBeUndefined();
|
|
164
|
-
expect(() => validateBoot(withoutInvite)).not.toThrow();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("app feature duplicating a bundled name is dropped (no createRegistry crash)", () => {
|
|
168
|
-
// create-kumiko-app's picker hands back createAuthEmailPasswordFeature()
|
|
169
|
-
// because the user ticked it in the recommended set; runDevApp then adds
|
|
170
|
-
// its OWN bundled copy via includeBundled:true, and createRegistry throws
|
|
171
|
-
// "Duplicate feature: auth-email-password". The dedupe path keeps the
|
|
172
|
-
// bundled instance (it carries authOptions wiring) and drops the app stub.
|
|
173
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
174
|
-
const features = composeFeatures([pickerAuthDupe, noopFeature], {
|
|
175
|
-
includeBundled: true,
|
|
176
|
-
});
|
|
177
|
-
// The warn is part of the fix contract (changeset: "warn so the user can
|
|
178
|
-
// remove the line") — without this assertion the warn is removable with no RED.
|
|
179
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("auth-email-password"));
|
|
180
|
-
warnSpy.mockRestore();
|
|
181
|
-
expect(features.map((f) => f.name)).toEqual([
|
|
182
|
-
"config",
|
|
183
|
-
"user",
|
|
184
|
-
"tenant",
|
|
185
|
-
"auth-email-password",
|
|
186
|
-
"noop-app",
|
|
187
|
-
]);
|
|
188
|
-
// The kept instance is the bundled one — confirmed by the presence of
|
|
189
|
-
// login/logout handlers (the picker stub has none).
|
|
190
|
-
const auth = features.find((f) => f.name === "auth-email-password");
|
|
191
|
-
expect(auth).toBeDefined();
|
|
192
|
-
if (!auth) return;
|
|
193
|
-
expect(Object.keys(auth.writeHandlers)).toContain("login");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("includeBundled=false → sessions mounted via preset + standalone collapses to one entry", () => {
|
|
197
|
-
const sessionsInstances = securityBaselineFeatures();
|
|
198
|
-
const standaloneSessions = createSessionsFeature();
|
|
199
|
-
const features = composeFeatures([...sessionsInstances, standaloneSessions], {
|
|
200
|
-
includeBundled: false,
|
|
201
|
-
});
|
|
202
|
-
const sessionsEntries = features.filter((f) => f.name === "sessions");
|
|
203
|
-
expect(sessionsEntries).toHaveLength(1);
|
|
204
|
-
expect(sessionsEntries[0]).toBe(sessionsInstances[0]);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// Pins the boot-wiring contract for config-seeds. runDevApp's onAfterSetup
|
|
2
|
-
// and runProdApp's seed-block both call `applyBootSeeds(...)` — this test
|
|
3
|
-
// calls the SAME helper, so if someone removes the call site from
|
|
4
|
-
// runDevApp / runProdApp the helper still has at least one caller (this
|
|
5
|
-
// test). Code review then sees an orphaned helper, not a silently broken
|
|
6
|
-
// boot. For a stricter end-to-end pin you'd start an actual runDevApp;
|
|
7
|
-
// that's heavy and not done here.
|
|
8
|
-
//
|
|
9
|
-
// Tests:
|
|
10
|
-
// 1. seed rows land in the projection after applyBootSeeds runs,
|
|
11
|
-
// 2. a re-boot is a no-op (idempotent),
|
|
12
|
-
// 3. an admin set on top of a seed wins the resolver cascade; coexistence
|
|
13
|
-
// vs. override semantics depend on the admin user's tenantId.
|
|
14
|
-
|
|
15
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
16
|
-
import {
|
|
17
|
-
configValuesTable,
|
|
18
|
-
createConfigAccessorFactory,
|
|
19
|
-
createConfigFeature,
|
|
20
|
-
createConfigResolver,
|
|
21
|
-
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
22
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
23
|
-
import {
|
|
24
|
-
access,
|
|
25
|
-
createSystemConfig,
|
|
26
|
-
createSystemSeed,
|
|
27
|
-
createTenantConfig,
|
|
28
|
-
createTenantSeed,
|
|
29
|
-
defineFeature,
|
|
30
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
31
|
-
import {
|
|
32
|
-
setupTestStack,
|
|
33
|
-
type TestStack,
|
|
34
|
-
TestUsers,
|
|
35
|
-
unsafePushTables,
|
|
36
|
-
} from "@cosmicdrift/kumiko-framework/stack";
|
|
37
|
-
import { applyBootSeeds } from "../boot/apply-boot-seeds";
|
|
38
|
-
|
|
39
|
-
const bootSeedsFeature = defineFeature("boot-seeds-test", (r) => {
|
|
40
|
-
r.requires("config");
|
|
41
|
-
r.config({
|
|
42
|
-
keys: {
|
|
43
|
-
siteName: createTenantConfig("text", {
|
|
44
|
-
default: "DEFAULT_SITE",
|
|
45
|
-
read: access.all,
|
|
46
|
-
write: access.all,
|
|
47
|
-
}),
|
|
48
|
-
maintenance: createSystemConfig("boolean", {
|
|
49
|
-
default: false,
|
|
50
|
-
read: access.all,
|
|
51
|
-
write: access.systemAdmin,
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
|
-
seeds: {
|
|
55
|
-
siteName: createTenantSeed({ value: "from-seed" }),
|
|
56
|
-
maintenance: createSystemSeed({ value: true }),
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const SITE_KEY = "boot-seeds-test:config:site-name";
|
|
62
|
-
const MAINT_KEY = "boot-seeds-test:config:maintenance";
|
|
63
|
-
|
|
64
|
-
let stack: TestStack;
|
|
65
|
-
const resolver = createConfigResolver();
|
|
66
|
-
|
|
67
|
-
beforeAll(async () => {
|
|
68
|
-
stack = await setupTestStack({
|
|
69
|
-
features: [createConfigFeature(), bootSeedsFeature],
|
|
70
|
-
extraContext: ({ registry }) => ({
|
|
71
|
-
configResolver: resolver,
|
|
72
|
-
_configAccessorFactory: createConfigAccessorFactory(registry, resolver),
|
|
73
|
-
}),
|
|
74
|
-
});
|
|
75
|
-
await unsafePushTables(stack.db, { configValuesTable });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterAll(async () => {
|
|
79
|
-
await stack.cleanup();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("config-seed boot wiring", () => {
|
|
83
|
-
test("first boot: applyBootSeeds writes one row per seed", async () => {
|
|
84
|
-
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
85
|
-
|
|
86
|
-
const rows = await selectMany(stack.db, configValuesTable);
|
|
87
|
-
expect(rows.length).toBe(2);
|
|
88
|
-
|
|
89
|
-
const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
|
|
90
|
-
expect(siteKeyDef).toBeDefined();
|
|
91
|
-
const sitePeek = await resolver.get(
|
|
92
|
-
SITE_KEY,
|
|
93
|
-
siteKeyDef!,
|
|
94
|
-
TestUsers.systemAdmin.tenantId,
|
|
95
|
-
TestUsers.systemAdmin.id,
|
|
96
|
-
stack.db,
|
|
97
|
-
);
|
|
98
|
-
expect(sitePeek).toBe("from-seed");
|
|
99
|
-
|
|
100
|
-
const maintKeyDef = stack.registry.getConfigKey(MAINT_KEY);
|
|
101
|
-
expect(maintKeyDef).toBeDefined();
|
|
102
|
-
const maintPeek = await resolver.get(
|
|
103
|
-
MAINT_KEY,
|
|
104
|
-
maintKeyDef!,
|
|
105
|
-
TestUsers.systemAdmin.tenantId,
|
|
106
|
-
TestUsers.systemAdmin.id,
|
|
107
|
-
stack.db,
|
|
108
|
-
);
|
|
109
|
-
expect(maintPeek).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("re-boot: idempotent — every seed already on disk → no extra rows", async () => {
|
|
113
|
-
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
114
|
-
|
|
115
|
-
const rows = await selectMany(stack.db, configValuesTable);
|
|
116
|
-
expect(rows.length).toBe(2);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("admin set on top of seed wins resolver — Re-Boot preserves admin", async () => {
|
|
120
|
-
// siteName is a TENANT-scope key. The seed writes a row under
|
|
121
|
-
// SYSTEM_TENANT_ID (= "for all tenants"). An admin on a real tenant
|
|
122
|
-
// writes a row under THAT tenantId — higher specificity. Both rows
|
|
123
|
-
// coexist; the resolver returns the more specific one.
|
|
124
|
-
//
|
|
125
|
-
// If the admin happens to write as SYSTEM_TENANT_ID (e.g. test user
|
|
126
|
-
// is the system-admin on the system-tenant), the admin write hits
|
|
127
|
-
// the seed-row directly and updates the same aggregate stream. Both
|
|
128
|
-
// paths end up with the admin value winning — the row-count
|
|
129
|
-
// assertion makes the path explicit.
|
|
130
|
-
await stack.http.writeOk(
|
|
131
|
-
"config:write:set",
|
|
132
|
-
{ key: SITE_KEY, value: "admin-override", scope: "tenant" },
|
|
133
|
-
TestUsers.systemAdmin,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
137
|
-
|
|
138
|
-
const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
|
|
139
|
-
expect(siteKeyDef).toBeDefined();
|
|
140
|
-
const peek = await resolver.get(
|
|
141
|
-
SITE_KEY,
|
|
142
|
-
siteKeyDef!,
|
|
143
|
-
TestUsers.systemAdmin.tenantId,
|
|
144
|
-
TestUsers.systemAdmin.id,
|
|
145
|
-
stack.db,
|
|
146
|
-
);
|
|
147
|
-
expect(peek).toBe("admin-override");
|
|
148
|
-
|
|
149
|
-
// Row-count tells us which path was hit:
|
|
150
|
-
// - 2 rows = override path (admin tenantId === SYSTEM_TENANT_ID,
|
|
151
|
-
// updated the seed-stream in place).
|
|
152
|
-
// - 3 rows = coexistence path (admin tenantId !== SYSTEM_TENANT_ID,
|
|
153
|
-
// new specific-tenant row sits next to the seed system-row).
|
|
154
|
-
// Either is correct as long as the resolver picks the admin value.
|
|
155
|
-
const rows = await selectMany(stack.db, configValuesTable);
|
|
156
|
-
expect([2, 3]).toContain(rows.length);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,112 +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 * as z from "zod";
|
|
5
|
-
import { assertPiiBootInvariants } from "../pii-boot-gate";
|
|
6
|
-
|
|
7
|
-
const piiFeature = defineFeature("gate-pii", (r) => {
|
|
8
|
-
r.entity(
|
|
9
|
-
"person",
|
|
10
|
-
createEntity({
|
|
11
|
-
table: "read_gate_persons",
|
|
12
|
-
fields: { email: createTextField({ required: true, personal: "self", find: "exact" }) },
|
|
13
|
-
}),
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
const plainFeature = defineFeature("gate-plain", (r) => {
|
|
17
|
-
r.entity(
|
|
18
|
-
"thing",
|
|
19
|
-
createEntity({
|
|
20
|
-
table: "read_gate_things",
|
|
21
|
-
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
22
|
-
}),
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Entity-free features: the gate must react to the event stance on its own,
|
|
27
|
-
// not ride along on an entity annotation (fw#2776).
|
|
28
|
-
const piiEventFeature = defineFeature("gate-pii-event", (r) => {
|
|
29
|
-
r.defineEvent("attempt", z.object({ userId: z.string(), email: z.string() }), {
|
|
30
|
-
piiFields: { email: { personal: { of: "userId" } } },
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
const noneEventFeature = defineFeature("gate-none-event", (r) => {
|
|
34
|
-
r.defineEvent("ping", z.object({ at: z.string() }), { piiFields: "none" });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const kms = new InMemoryKmsAdapter();
|
|
38
|
-
const KEY = Buffer.alloc(32, 7).toString("base64");
|
|
39
|
-
|
|
40
|
-
describe("assertPiiBootInvariants — prod", () => {
|
|
41
|
-
test("PII without a KMS aborts boot", () => {
|
|
42
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod" })).toThrow(
|
|
43
|
-
/BOOT ABORTED.*PLAINTEXT.*allowPlaintextPii/s,
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("explicit allowPlaintextPii downgrades to a warning", () => {
|
|
48
|
-
expect(() =>
|
|
49
|
-
assertPiiBootInvariants([piiFeature], {
|
|
50
|
-
mode: "prod",
|
|
51
|
-
allowPlaintextPii: "kms rollout pending, infra#188",
|
|
52
|
-
}),
|
|
53
|
-
).not.toThrow();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("KMS + blindIndexKey boots", () => {
|
|
57
|
-
expect(() =>
|
|
58
|
-
assertPiiBootInvariants([piiFeature], { mode: "prod", kms, blindIndexKey: KEY }),
|
|
59
|
-
).not.toThrow();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("KMS without blindIndexKey aborts when lookupable fields exist", () => {
|
|
63
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod", kms })).toThrow(
|
|
64
|
-
/blindIndexKey/,
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("no PII entities → nothing to gate", () => {
|
|
69
|
-
expect(() => assertPiiBootInvariants([plainFeature], { mode: "prod" })).not.toThrow();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('an entity-free feature with a non-"none" event stance aborts boot without a KMS', () => {
|
|
73
|
-
expect(() => assertPiiBootInvariants([piiEventFeature], { mode: "prod" })).toThrow(
|
|
74
|
-
/BOOT ABORTED.*gate-pii-event:event:attempt.*PLAINTEXT/s,
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('events declaring piiFields: "none" are not gated', () => {
|
|
79
|
-
expect(() => assertPiiBootInvariants([noneEventFeature], { mode: "prod" })).not.toThrow();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("a KMS satisfies the event gate", () => {
|
|
83
|
-
expect(() =>
|
|
84
|
-
assertPiiBootInvariants([piiEventFeature], { mode: "prod", kms, blindIndexKey: KEY }),
|
|
85
|
-
).not.toThrow();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("allowPlaintextPii acknowledges the event gate too", () => {
|
|
89
|
-
expect(() =>
|
|
90
|
-
assertPiiBootInvariants([piiEventFeature], {
|
|
91
|
-
mode: "prod",
|
|
92
|
-
allowPlaintextPii: "kms rollout pending",
|
|
93
|
-
}),
|
|
94
|
-
).not.toThrow();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("assertPiiBootInvariants — dev", () => {
|
|
99
|
-
test("PII without a KMS only warns in dev", () => {
|
|
100
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev" })).not.toThrow();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("KMS without blindIndexKey aborts in dev too (lookups broken in any mode)", () => {
|
|
104
|
-
expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev", kms })).toThrow(
|
|
105
|
-
/blindIndexKey/,
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("a PII event without a KMS only warns in dev", () => {
|
|
110
|
-
expect(() => assertPiiBootInvariants([piiEventFeature], { mode: "dev" })).not.toThrow();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -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,77 +0,0 @@
|
|
|
1
|
-
// The app theme plugin only works if its base-layer output lands AFTER the framework's light
|
|
2
|
-
// palette (`@layer base :root:not(.dark)`); same selector, same layer, so source order decides.
|
|
3
|
-
// This runs the real Tailwind one-shot over an app stylesheet that imports the framework CSS and
|
|
4
|
-
// loads a theme module through the published `theme-plugin` subpath.
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from "bun:test";
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { runTailwindOnce } from "../build-prod-bundle";
|
|
12
|
-
|
|
13
|
-
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
14
|
-
const FRAMEWORK_LIGHT_PRIMARY = "--color-primary:#1a1c1e";
|
|
15
|
-
|
|
16
|
-
function bunAvailable(): boolean {
|
|
17
|
-
try {
|
|
18
|
-
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
19
|
-
return true;
|
|
20
|
-
} catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("renderer-web theme plugin in a real Tailwind build", () => {
|
|
26
|
-
test.skipIf(!bunAvailable())(
|
|
27
|
-
"app theme overrides the framework light palette, sets dark values and adds utilities for new colors",
|
|
28
|
-
async () => {
|
|
29
|
-
const dir = await mkdtemp(join(REPO_ROOT, ".theme-plugin-"));
|
|
30
|
-
try {
|
|
31
|
-
await writeFile(
|
|
32
|
-
join(dir, "theme.ts"),
|
|
33
|
-
`import { createThemePlugin, defineAppTheme } from "@cosmicdrift/kumiko-renderer-web/theme-plugin";
|
|
34
|
-
export default createThemePlugin(defineAppTheme({
|
|
35
|
-
colors: {
|
|
36
|
-
primary: { light: "#0a7d5a", dark: "#34d399" },
|
|
37
|
-
"brand-soft": "#e0f5ee",
|
|
38
|
-
},
|
|
39
|
-
shadows: { card: "none" },
|
|
40
|
-
fonts: { sans: "Manrope, sans-serif" },
|
|
41
|
-
}));
|
|
42
|
-
`,
|
|
43
|
-
);
|
|
44
|
-
await writeFile(
|
|
45
|
-
join(dir, "probe.tsx"),
|
|
46
|
-
`export const Probe = () => <div className="bg-brand-soft text-primary" />;\n`,
|
|
47
|
-
);
|
|
48
|
-
await writeFile(
|
|
49
|
-
join(dir, "styles.css"),
|
|
50
|
-
`@import "@cosmicdrift/kumiko-renderer-web/styles.css";\n@plugin "./theme.ts";\n@source "./probe.tsx";\n`,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const css = await runTailwindOnce(join(dir, "styles.css"), dir);
|
|
54
|
-
|
|
55
|
-
const frameworkLight = css.indexOf(FRAMEWORK_LIGHT_PRIMARY);
|
|
56
|
-
const appLight = css.indexOf("--color-primary:#0a7d5a");
|
|
57
|
-
expect(frameworkLight).toBeGreaterThan(-1);
|
|
58
|
-
expect(appLight).toBeGreaterThan(frameworkLight);
|
|
59
|
-
|
|
60
|
-
expect(css).toMatch(/\.dark\{[^}]*--color-primary:#34d399/);
|
|
61
|
-
expect(css).toMatch(/\.bg-brand-soft\{background-color:var\(--color-brand-soft\)\}/);
|
|
62
|
-
|
|
63
|
-
const frameworkCardShadow = css.indexOf("--card-shadow:0 1px 3px");
|
|
64
|
-
const appCardShadow = css.indexOf("--card-shadow:none");
|
|
65
|
-
expect(frameworkCardShadow).toBeGreaterThan(-1);
|
|
66
|
-
expect(appCardShadow).toBeGreaterThan(frameworkCardShadow);
|
|
67
|
-
// An unlayered framework :root would beat the plugin's base layer despite the order.
|
|
68
|
-
expect(css).toMatch(/:root\{[^}]*--kumiko-grid-row-h/);
|
|
69
|
-
expect(css).not.toMatch(/:root\{[^}]*--kumiko-grid-row-h[^}]*--card-/);
|
|
70
|
-
expect(css).not.toMatch(/:root\{[^}]*--card-[^}]*--kumiko-grid-row-h/);
|
|
71
|
-
expect(css).toMatch(/body\{font-family:var\(--font-sans\)\}/);
|
|
72
|
-
} finally {
|
|
73
|
-
await rm(dir, { recursive: true, force: true });
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
);
|
|
77
|
-
});
|