@cosmicdrift/kumiko-framework 0.304.0 → 0.306.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 +4 -4
- package/src/api/__tests__/extra-route-rejection.test.ts +38 -0
- package/src/api/__tests__/extra-routes.integration.test.ts +30 -0
- package/src/api/__tests__/server-boot-guards.test.ts +1 -0
- package/src/api/__tests__/server-error-logging.test.ts +104 -0
- package/src/api/api-constants.ts +13 -0
- package/src/api/extra-route.ts +33 -4
- package/src/api/index.ts +1 -0
- package/src/api/request-context.ts +5 -4
- package/src/api/routes.ts +26 -1
- package/src/api/server.ts +8 -2
- package/src/bun-db/__tests__/closed-connection-retry.integration.test.ts +159 -0
- package/src/bun-db/__tests__/select-many-retry.integration.test.ts +138 -0
- package/src/bun-db/query.ts +42 -18
- package/src/changes.json +108 -0
- package/src/db/__tests__/pg-error.test.ts +14 -0
- package/src/db/__tests__/system-db-view-export.test.ts +107 -0
- package/src/db/__tests__/tenant-db-no-raw.test.ts +10 -0
- package/src/db/__tests__/with-systemdb-unsafe-raw-grant.test.ts +71 -0
- package/src/db/event-store-executor-write.ts +7 -0
- package/src/db/index.ts +1 -1
- package/src/db/pg-error.ts +13 -0
- package/src/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +28 -28
- package/src/db/tenant-db.ts +140 -16
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +6 -1
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator.test.ts +1 -1
- package/src/engine/__tests__/tier-resolver-extension.test.ts +1 -1
- package/src/engine/boot-validator/access-declarations.ts +5 -66
- package/src/engine/extension-names.ts +55 -25
- package/src/engine/extensions/storage-provider.ts +14 -41
- package/src/engine/extensions/tenant-data.ts +4 -0
- package/src/engine/extensions/tenant-resource.ts +40 -0
- package/src/engine/extensions/user-data.ts +8 -7
- package/src/engine/feature-ast/__tests__/handler-header-roundtrip.test.ts +669 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +5 -3
- package/src/engine/feature-ast/__tests__/patch-update.test.ts +718 -0
- package/src/engine/feature-ast/__tests__/pattern-change-schema.test.ts +819 -0
- package/src/engine/feature-ast/entity-field-types.ts +41 -0
- package/src/engine/feature-ast/extractors/handlers.ts +217 -84
- package/src/engine/feature-ast/extractors/hooks.ts +72 -15
- package/src/engine/feature-ast/extractors/round2.ts +21 -0
- package/src/engine/feature-ast/extractors/shared.ts +9 -0
- package/src/engine/feature-ast/index.ts +11 -1
- package/src/engine/feature-ast/patch.ts +338 -5
- package/src/engine/feature-ast/patcher.ts +2 -2
- package/src/engine/feature-ast/pattern-change-schema.ts +1411 -0
- package/src/engine/feature-ast/patterns.ts +22 -15
- package/src/engine/feature-ast/render.ts +1 -0
- package/src/engine/feature-ui-extensions.ts +8 -7
- package/src/engine/index.ts +23 -5
- package/src/engine/personal-data-fields.ts +66 -0
- package/src/engine/registry-validate.ts +15 -0
- package/src/engine/registry.ts +2 -0
- package/src/engine/types/extension-options-map.ts +1 -0
- package/src/engine/types/index.ts +8 -0
- package/src/env/__tests__/dry-run.test.ts +43 -3
- package/src/env/dry-run.ts +28 -15
- package/src/errors/__tests__/write-failures.test.ts +47 -4
- package/src/errors/i18n/de.yaml +12 -0
- package/src/errors/i18n/en.yaml +12 -0
- package/src/errors/reasons.ts +4 -0
- package/src/errors/write-error-info.ts +12 -3
- package/src/jobs/__tests__/job-backoff.integration.test.ts +155 -0
- package/src/jobs/__tests__/job-retention.integration.test.ts +316 -0
- package/src/jobs/__tests__/job-retry-enqueue-paths.integration.test.ts +309 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +38 -3
- package/src/jobs/job-runner.ts +170 -19
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +113 -26
- package/src/pipeline/__tests__/dispatcher.test.ts +5 -0
- package/src/pipeline/__tests__/hook-systemdb-escape-hatch.integration.test.ts +246 -0
- package/src/pipeline/__tests__/idempotency-transient-failure.integration.test.ts +198 -0
- package/src/pipeline/__tests__/public-intake-runtime-gate.integration.test.ts +425 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.test.ts +59 -0
- package/src/pipeline/active-membership.ts +5 -1
- package/src/pipeline/dispatch-batch.ts +59 -13
- package/src/pipeline/dispatch-query.ts +16 -5
- package/src/pipeline/dispatch-shared.ts +12 -5
- package/src/pipeline/dispatch-stream.ts +7 -2
- package/src/pipeline/dispatch-write.ts +22 -5
- package/src/pipeline/dispatcher.ts +9 -2
- package/src/pipeline/idempotency.ts +16 -0
- package/src/pipeline/member-reader.ts +3 -1
- package/src/pipeline/system-identity-switch.ts +22 -4
- package/src/pipeline/write-origin.ts +107 -0
- package/src/rate-limit/__tests__/middleware.integration.test.ts +40 -0
- package/src/rate-limit/middleware.ts +3 -0
- package/src/stack/__tests__/setup-test-stack-metrics.integration.test.ts +79 -0
- package/src/stack/test-stack.ts +5 -0
- package/src/testing/closed-connection-error.ts +62 -0
- package/src/testing/index.ts +1 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +0 -79
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
// Handler-header fields (access/rateLimit/escapeHatch/agent) authored as a
|
|
2
|
+
// non-literal reference must round-trip verbatim as a RawRefSentinel, or,
|
|
3
|
+
// for a fully literal but unrecognized value, report a ParseError.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { Project } from "ts-morph";
|
|
7
|
+
import { parseSourceFile } from "../parse";
|
|
8
|
+
import type { FeaturePattern } from "../patterns";
|
|
9
|
+
import { renderFeatureFile } from "../render";
|
|
10
|
+
|
|
11
|
+
let fileCounter = 0;
|
|
12
|
+
|
|
13
|
+
function parse(source: string) {
|
|
14
|
+
const project = new Project({
|
|
15
|
+
skipAddingFilesFromTsConfig: true,
|
|
16
|
+
skipFileDependencyResolution: true,
|
|
17
|
+
useInMemoryFileSystem: true,
|
|
18
|
+
});
|
|
19
|
+
fileCounter += 1;
|
|
20
|
+
const sf = project.createSourceFile(`f-${fileCounter}.ts`, source);
|
|
21
|
+
const result = parseSourceFile(sf);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findPattern<K extends FeaturePattern["kind"]>(
|
|
26
|
+
patterns: readonly FeaturePattern[],
|
|
27
|
+
kind: K,
|
|
28
|
+
): Extract<FeaturePattern, { kind: K }> {
|
|
29
|
+
const found = patterns.find((p) => p.kind === kind);
|
|
30
|
+
if (!found) throw new Error(`no ${kind} pattern found`);
|
|
31
|
+
return found as Extract<FeaturePattern, { kind: K }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_IMPORTS = [
|
|
35
|
+
'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
|
|
36
|
+
'import { z } from "zod";',
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
describe("access: ADMIN, imported const (object form)", () => {
|
|
40
|
+
const source = `
|
|
41
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
import { ADMIN } from "./access-consts";
|
|
44
|
+
|
|
45
|
+
defineFeature("f", (r) => {
|
|
46
|
+
r.writeHandler({
|
|
47
|
+
name: "x",
|
|
48
|
+
schema: z.object({}),
|
|
49
|
+
handler: async () => {},
|
|
50
|
+
access: ADMIN,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
`;
|
|
54
|
+
const result = parse(source);
|
|
55
|
+
|
|
56
|
+
test("parses with no errors and keeps access as a raw sentinel", () => {
|
|
57
|
+
expect(result.errors).toEqual([]);
|
|
58
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
59
|
+
expect(pattern).toMatchObject({ access: { __raw: "ADMIN" } });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("render → parse roundtrip keeps the reference verbatim", () => {
|
|
63
|
+
const rendered = renderFeatureFile({
|
|
64
|
+
featureName: result.featureName ?? "",
|
|
65
|
+
patterns: result.patterns,
|
|
66
|
+
imports: [...DEFAULT_IMPORTS, 'import { ADMIN } from "./access-consts";'],
|
|
67
|
+
});
|
|
68
|
+
expect(rendered).toContain("access: ADMIN,");
|
|
69
|
+
const reparsed = parse(rendered);
|
|
70
|
+
expect(reparsed.errors).toEqual([]);
|
|
71
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
72
|
+
expect(reparsedPattern).toMatchObject({ access: { __raw: "ADMIN" } });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('access: LOCAL, same-file const { roles: ["Admin"] }', () => {
|
|
77
|
+
const source = `
|
|
78
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
|
|
81
|
+
const LOCAL = { roles: ["Admin"] };
|
|
82
|
+
|
|
83
|
+
defineFeature("f", (r) => {
|
|
84
|
+
r.writeHandler({
|
|
85
|
+
name: "x",
|
|
86
|
+
schema: z.object({}),
|
|
87
|
+
handler: async () => {},
|
|
88
|
+
access: LOCAL,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
const result = parse(source);
|
|
93
|
+
|
|
94
|
+
test("keeps access as a raw sentinel even though LOCAL is same-file", () => {
|
|
95
|
+
expect(result.errors).toEqual([]);
|
|
96
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
97
|
+
expect(pattern).toMatchObject({ access: { __raw: "LOCAL" } });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("render → parse roundtrip keeps the reference verbatim", () => {
|
|
101
|
+
const rendered = renderFeatureFile({
|
|
102
|
+
featureName: result.featureName ?? "",
|
|
103
|
+
patterns: result.patterns,
|
|
104
|
+
imports: [...DEFAULT_IMPORTS, 'const LOCAL = { roles: ["Admin"] };'],
|
|
105
|
+
});
|
|
106
|
+
expect(rendered).toContain("access: LOCAL,");
|
|
107
|
+
const reparsed = parse(rendered);
|
|
108
|
+
expect(reparsed.errors).toEqual([]);
|
|
109
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
110
|
+
expect(reparsedPattern).toMatchObject({ access: { __raw: "LOCAL" } });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("access: { roles: [ROLE_X] }, partially-literal object", () => {
|
|
115
|
+
const source = `
|
|
116
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
import { ROLE_X } from "./roles";
|
|
119
|
+
|
|
120
|
+
defineFeature("f", (r) => {
|
|
121
|
+
r.writeHandler({
|
|
122
|
+
name: "x",
|
|
123
|
+
schema: z.object({}),
|
|
124
|
+
handler: async () => {},
|
|
125
|
+
access: { roles: [ROLE_X] },
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
`;
|
|
129
|
+
const result = parse(source);
|
|
130
|
+
|
|
131
|
+
test("keeps the whole initializer verbatim, not just ROLE_X", () => {
|
|
132
|
+
expect(result.errors).toEqual([]);
|
|
133
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
134
|
+
expect(pattern).toMatchObject({ access: { __raw: "{ roles: [ROLE_X] }" } });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("render → parse roundtrip keeps the reference verbatim", () => {
|
|
138
|
+
const rendered = renderFeatureFile({
|
|
139
|
+
featureName: result.featureName ?? "",
|
|
140
|
+
patterns: result.patterns,
|
|
141
|
+
imports: [...DEFAULT_IMPORTS, 'import { ROLE_X } from "./roles";'],
|
|
142
|
+
});
|
|
143
|
+
expect(rendered).toContain("access: { roles: [ROLE_X] },");
|
|
144
|
+
const reparsed = parse(rendered);
|
|
145
|
+
expect(reparsed.errors).toEqual([]);
|
|
146
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
147
|
+
expect(reparsedPattern).toMatchObject({ access: { __raw: "{ roles: [ROLE_X] }" } });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('rateLimit: { disabled: true, reason: "x" }, fully literal RateLimitDisabled', () => {
|
|
152
|
+
const source = `
|
|
153
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
154
|
+
import { z } from "zod";
|
|
155
|
+
|
|
156
|
+
defineFeature("f", (r) => {
|
|
157
|
+
r.writeHandler({
|
|
158
|
+
name: "x",
|
|
159
|
+
schema: z.object({}),
|
|
160
|
+
handler: async () => {},
|
|
161
|
+
access: { roles: ["Admin"] },
|
|
162
|
+
rateLimit: { disabled: true, reason: "x" },
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
`;
|
|
166
|
+
const result = parse(source);
|
|
167
|
+
|
|
168
|
+
test("parses into a structured RateLimitDisabled value, not a raw sentinel", () => {
|
|
169
|
+
expect(result.errors).toEqual([]);
|
|
170
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
171
|
+
expect(pattern).toMatchObject({ rateLimit: { disabled: true, reason: "x" } });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("render → parse roundtrip preserves the structured value", () => {
|
|
175
|
+
const rendered = renderFeatureFile({
|
|
176
|
+
featureName: result.featureName ?? "",
|
|
177
|
+
patterns: result.patterns,
|
|
178
|
+
});
|
|
179
|
+
const reparsed = parse(rendered);
|
|
180
|
+
expect(reparsed.errors).toEqual([]);
|
|
181
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
182
|
+
expect(reparsedPattern).toMatchObject({ rateLimit: { disabled: true, reason: "x" } });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('access: { roles: ["anonymous"], personalData: PD }, non-literal sub-value', () => {
|
|
187
|
+
const source = `
|
|
188
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
189
|
+
import { z } from "zod";
|
|
190
|
+
import { PD } from "./pd";
|
|
191
|
+
|
|
192
|
+
defineFeature("f", (r) => {
|
|
193
|
+
r.writeHandler({
|
|
194
|
+
name: "x",
|
|
195
|
+
schema: z.object({}),
|
|
196
|
+
handler: async () => {},
|
|
197
|
+
access: { roles: ["anonymous"], personalData: PD },
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
`;
|
|
201
|
+
const result = parse(source);
|
|
202
|
+
|
|
203
|
+
test("keeps the whole initializer verbatim instead of narrowing to { roles }", () => {
|
|
204
|
+
expect(result.errors).toEqual([]);
|
|
205
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
206
|
+
expect(pattern).toMatchObject({
|
|
207
|
+
access: { __raw: '{ roles: ["anonymous"], personalData: PD }' },
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("shorthand access property, value previously lost", () => {
|
|
213
|
+
const source = `
|
|
214
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
215
|
+
import { z } from "zod";
|
|
216
|
+
|
|
217
|
+
const access = { roles: ["Admin"] };
|
|
218
|
+
|
|
219
|
+
defineFeature("f", (r) => {
|
|
220
|
+
r.writeHandler({
|
|
221
|
+
name: "x",
|
|
222
|
+
schema: z.object({}),
|
|
223
|
+
handler: async () => {},
|
|
224
|
+
access,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
`;
|
|
228
|
+
const result = parse(source);
|
|
229
|
+
|
|
230
|
+
test("access is kept as a raw sentinel referencing the shorthand binding", () => {
|
|
231
|
+
expect(result.errors).toEqual([]);
|
|
232
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
233
|
+
expect(pattern).toMatchObject({ access: { __raw: "access" } });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("positional form, both access and rateLimit authored as references", () => {
|
|
238
|
+
const source = `
|
|
239
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
240
|
+
import { z } from "zod";
|
|
241
|
+
import { ADMIN } from "./access-consts";
|
|
242
|
+
import { LIMIT } from "./rate-limits";
|
|
243
|
+
|
|
244
|
+
defineFeature("f", (r) => {
|
|
245
|
+
r.writeHandler("x", z.object({}), async () => {}, { access: ADMIN, rateLimit: LIMIT });
|
|
246
|
+
});
|
|
247
|
+
`;
|
|
248
|
+
const result = parse(source);
|
|
249
|
+
|
|
250
|
+
test("both header fields are kept as raw sentinels", () => {
|
|
251
|
+
expect(result.errors).toEqual([]);
|
|
252
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
253
|
+
expect(pattern).toMatchObject({
|
|
254
|
+
access: { __raw: "ADMIN" },
|
|
255
|
+
rateLimit: { __raw: "LIMIT" },
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("render → parse roundtrip keeps both references verbatim", () => {
|
|
260
|
+
const rendered = renderFeatureFile({
|
|
261
|
+
featureName: result.featureName ?? "",
|
|
262
|
+
patterns: result.patterns,
|
|
263
|
+
imports: [
|
|
264
|
+
...DEFAULT_IMPORTS,
|
|
265
|
+
'import { ADMIN } from "./access-consts";',
|
|
266
|
+
'import { LIMIT } from "./rate-limits";',
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
expect(rendered).toContain("access: ADMIN,");
|
|
270
|
+
expect(rendered).toContain("rateLimit: LIMIT,");
|
|
271
|
+
const reparsed = parse(rendered);
|
|
272
|
+
expect(reparsed.errors).toEqual([]);
|
|
273
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
274
|
+
expect(reparsedPattern).toMatchObject({
|
|
275
|
+
access: { __raw: "ADMIN" },
|
|
276
|
+
rateLimit: { __raw: "LIMIT" },
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("positional form, options argument is a bare identifier", () => {
|
|
282
|
+
const source = `
|
|
283
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
284
|
+
import { z } from "zod";
|
|
285
|
+
|
|
286
|
+
const OPTS = { access: { roles: ["Admin"] } };
|
|
287
|
+
|
|
288
|
+
defineFeature("f", (r) => {
|
|
289
|
+
r.writeHandler("x", z.object({}), async () => {}, OPTS);
|
|
290
|
+
});
|
|
291
|
+
`;
|
|
292
|
+
const result = parse(source);
|
|
293
|
+
|
|
294
|
+
test("kept as an opaque pattern instead of silently dropping the header", () => {
|
|
295
|
+
expect(result.errors).toEqual([]);
|
|
296
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
297
|
+
expect(pattern.handlerName).toBeUndefined();
|
|
298
|
+
expect((pattern as { source: { raw: string } }).source.raw).toContain(
|
|
299
|
+
'r.writeHandler("x", z.object({}), async () => {}, OPTS)',
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("render → parse roundtrip keeps the call verbatim", () => {
|
|
304
|
+
const rendered = renderFeatureFile({
|
|
305
|
+
featureName: result.featureName ?? "",
|
|
306
|
+
patterns: result.patterns,
|
|
307
|
+
imports: [...DEFAULT_IMPORTS, 'const OPTS = { access: { roles: ["Admin"] } };'],
|
|
308
|
+
});
|
|
309
|
+
expect(rendered).toContain('r.writeHandler("x", z.object({}), async () => {}, OPTS)');
|
|
310
|
+
const reparsed = parse(rendered);
|
|
311
|
+
expect(reparsed.errors).toEqual([]);
|
|
312
|
+
const reparsedPattern = findPattern(reparsed.patterns, "writeHandler");
|
|
313
|
+
expect(reparsedPattern.handlerName).toBeUndefined();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("fully literal but unrecognized access shape", () => {
|
|
318
|
+
const source = `
|
|
319
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
320
|
+
import { z } from "zod";
|
|
321
|
+
|
|
322
|
+
defineFeature("f", (r) => {
|
|
323
|
+
r.writeHandler({
|
|
324
|
+
name: "x",
|
|
325
|
+
schema: z.object({}),
|
|
326
|
+
handler: async () => {},
|
|
327
|
+
access: { roles: "Admin" },
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
`;
|
|
331
|
+
const result = parse(source);
|
|
332
|
+
|
|
333
|
+
test("ParseErrors instead of silently dropping access", () => {
|
|
334
|
+
expect(result.patterns.some((p) => p.kind === "writeHandler")).toBe(false);
|
|
335
|
+
expect(result.errors).toHaveLength(1);
|
|
336
|
+
expect(result.errors[0]?.methodName).toBe("writeHandler");
|
|
337
|
+
expect(result.errors[0]?.reason).toContain("access");
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("escapeHatch: { reason: REASON }, non-literal reason", () => {
|
|
342
|
+
const source = `
|
|
343
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
344
|
+
import { z } from "zod";
|
|
345
|
+
import { REASON } from "./reasons";
|
|
346
|
+
|
|
347
|
+
defineFeature("f", (r) => {
|
|
348
|
+
r.writeHandler({
|
|
349
|
+
name: "x",
|
|
350
|
+
schema: z.object({}),
|
|
351
|
+
handler: async () => {},
|
|
352
|
+
access: { roles: ["Admin"] },
|
|
353
|
+
escapeHatch: { reason: REASON },
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
`;
|
|
357
|
+
const result = parse(source);
|
|
358
|
+
|
|
359
|
+
test("keeps the whole initializer verbatim", () => {
|
|
360
|
+
expect(result.errors).toEqual([]);
|
|
361
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
362
|
+
expect(pattern).toMatchObject({ escapeHatch: { __raw: "{ reason: REASON }" } });
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("streamHandler with a literal escapeHatch", () => {
|
|
367
|
+
const source = `
|
|
368
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
369
|
+
import { z } from "zod";
|
|
370
|
+
|
|
371
|
+
defineFeature("f", (r) => {
|
|
372
|
+
r.streamHandler({
|
|
373
|
+
name: "x",
|
|
374
|
+
schema: z.object({}),
|
|
375
|
+
handler: async function* () { yield "token"; },
|
|
376
|
+
access: { roles: ["Admin"] },
|
|
377
|
+
escapeHatch: { reason: "r" },
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
`;
|
|
381
|
+
const result = parse(source);
|
|
382
|
+
|
|
383
|
+
test("escapeHatch is kept as a structured value", () => {
|
|
384
|
+
expect(result.errors).toEqual([]);
|
|
385
|
+
const pattern = findPattern(result.patterns, "streamHandler");
|
|
386
|
+
expect(pattern).toMatchObject({ escapeHatch: { reason: "r" } });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("render → parse roundtrip keeps escapeHatch", () => {
|
|
390
|
+
const rendered = renderFeatureFile({
|
|
391
|
+
featureName: result.featureName ?? "",
|
|
392
|
+
patterns: result.patterns,
|
|
393
|
+
});
|
|
394
|
+
expect(rendered).toContain('escapeHatch: { reason: "r" },');
|
|
395
|
+
const reparsed = parse(rendered);
|
|
396
|
+
expect(reparsed.errors).toEqual([]);
|
|
397
|
+
const reparsedPattern = findPattern(reparsed.patterns, "streamHandler");
|
|
398
|
+
expect(reparsedPattern).toMatchObject({ escapeHatch: { reason: "r" } });
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("queryHandler with raw access", () => {
|
|
403
|
+
const source = `
|
|
404
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
405
|
+
import { z } from "zod";
|
|
406
|
+
import { ADMIN } from "./access-consts";
|
|
407
|
+
|
|
408
|
+
defineFeature("f", (r) => {
|
|
409
|
+
r.queryHandler({
|
|
410
|
+
name: "x",
|
|
411
|
+
schema: z.object({}),
|
|
412
|
+
handler: async () => ({}),
|
|
413
|
+
access: ADMIN,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
`;
|
|
417
|
+
const result = parse(source);
|
|
418
|
+
|
|
419
|
+
test("keeps access as a raw sentinel", () => {
|
|
420
|
+
expect(result.errors).toEqual([]);
|
|
421
|
+
const pattern = findPattern(result.patterns, "queryHandler");
|
|
422
|
+
expect(pattern).toMatchObject({ access: { __raw: "ADMIN" } });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("render → parse roundtrip keeps the reference verbatim", () => {
|
|
426
|
+
const rendered = renderFeatureFile({
|
|
427
|
+
featureName: result.featureName ?? "",
|
|
428
|
+
patterns: result.patterns,
|
|
429
|
+
imports: [...DEFAULT_IMPORTS, 'import { ADMIN } from "./access-consts";'],
|
|
430
|
+
});
|
|
431
|
+
expect(rendered).toContain("access: ADMIN,");
|
|
432
|
+
const reparsed = parse(rendered);
|
|
433
|
+
expect(reparsed.errors).toEqual([]);
|
|
434
|
+
const reparsedPattern = findPattern(reparsed.patterns, "queryHandler");
|
|
435
|
+
expect(reparsedPattern).toMatchObject({ access: { __raw: "ADMIN" } });
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("hook escapeHatch: { reason: REASON }, non-literal reason", () => {
|
|
440
|
+
const source = `
|
|
441
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
442
|
+
import { z } from "zod";
|
|
443
|
+
import { REASON } from "./reasons";
|
|
444
|
+
|
|
445
|
+
defineFeature("f", (r) => {
|
|
446
|
+
r.hook("postSave", "task", async (event, ctx) => {}, { escapeHatch: { reason: REASON } });
|
|
447
|
+
});
|
|
448
|
+
`;
|
|
449
|
+
const result = parse(source);
|
|
450
|
+
|
|
451
|
+
test("keeps the whole initializer verbatim", () => {
|
|
452
|
+
expect(result.errors).toEqual([]);
|
|
453
|
+
const pattern = findPattern(result.patterns, "hook");
|
|
454
|
+
expect(pattern).toMatchObject({ escapeHatch: { __raw: "{ reason: REASON }" } });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("render → parse roundtrip keeps the reference verbatim", () => {
|
|
458
|
+
const rendered = renderFeatureFile({
|
|
459
|
+
featureName: result.featureName ?? "",
|
|
460
|
+
patterns: result.patterns,
|
|
461
|
+
imports: [...DEFAULT_IMPORTS, 'import { REASON } from "./reasons";'],
|
|
462
|
+
});
|
|
463
|
+
expect(rendered).toContain("escapeHatch: { reason: REASON },");
|
|
464
|
+
const reparsed = parse(rendered);
|
|
465
|
+
expect(reparsed.errors).toEqual([]);
|
|
466
|
+
const reparsedPattern = findPattern(reparsed.patterns, "hook");
|
|
467
|
+
expect(reparsedPattern).toMatchObject({ escapeHatch: { __raw: "{ reason: REASON }" } });
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe("hook shorthand escapeHatch property", () => {
|
|
472
|
+
const source = `
|
|
473
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
474
|
+
import { z } from "zod";
|
|
475
|
+
|
|
476
|
+
const escapeHatch = { reason: "bulk reindex" };
|
|
477
|
+
|
|
478
|
+
defineFeature("f", (r) => {
|
|
479
|
+
r.hook("postSave", "task", async (event, ctx) => {}, { escapeHatch });
|
|
480
|
+
});
|
|
481
|
+
`;
|
|
482
|
+
const result = parse(source);
|
|
483
|
+
|
|
484
|
+
test("escapeHatch is kept as a raw sentinel referencing the shorthand binding", () => {
|
|
485
|
+
expect(result.errors).toEqual([]);
|
|
486
|
+
const pattern = findPattern(result.patterns, "hook");
|
|
487
|
+
expect(pattern).toMatchObject({ escapeHatch: { __raw: "escapeHatch" } });
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("existing literal cases still produce structured values", () => {
|
|
492
|
+
const source = `
|
|
493
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
494
|
+
import { z } from "zod";
|
|
495
|
+
|
|
496
|
+
defineFeature("f", (r) => {
|
|
497
|
+
r.writeHandler({
|
|
498
|
+
name: "x",
|
|
499
|
+
schema: z.object({}),
|
|
500
|
+
handler: async () => {},
|
|
501
|
+
access: { roles: ["Admin"] },
|
|
502
|
+
rateLimit: { per: "user", limit: 5, windowSeconds: 60 },
|
|
503
|
+
agent: { expose: true, risk: "high" },
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
`;
|
|
507
|
+
const result = parse(source);
|
|
508
|
+
|
|
509
|
+
test("access/rateLimit/agent are plain structured values, no sentinel", () => {
|
|
510
|
+
expect(result.errors).toEqual([]);
|
|
511
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
512
|
+
expect(pattern).toMatchObject({
|
|
513
|
+
access: { roles: ["Admin"] },
|
|
514
|
+
rateLimit: { per: "user", limit: 5, windowSeconds: 60 },
|
|
515
|
+
agent: { expose: true, risk: "high" },
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("object form with a top-level spread", () => {
|
|
521
|
+
const source = `
|
|
522
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
523
|
+
import { z } from "zod";
|
|
524
|
+
|
|
525
|
+
const BASE = { description: "shared" };
|
|
526
|
+
|
|
527
|
+
defineFeature("f", (r) => {
|
|
528
|
+
r.writeHandler({
|
|
529
|
+
...BASE,
|
|
530
|
+
name: "x",
|
|
531
|
+
schema: z.object({}),
|
|
532
|
+
handler: async () => {},
|
|
533
|
+
access: { roles: ["Admin"] },
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
`;
|
|
537
|
+
const result = parse(source);
|
|
538
|
+
|
|
539
|
+
test("kept as an opaque pattern instead of dropping the spread", () => {
|
|
540
|
+
expect(result.errors).toEqual([]);
|
|
541
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
542
|
+
expect(pattern.handlerName).toBeUndefined();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("render → parse roundtrip keeps the call verbatim", () => {
|
|
546
|
+
const rendered = renderFeatureFile({
|
|
547
|
+
featureName: result.featureName ?? "",
|
|
548
|
+
patterns: result.patterns,
|
|
549
|
+
imports: [...DEFAULT_IMPORTS, 'const BASE = { description: "shared" };'],
|
|
550
|
+
});
|
|
551
|
+
expect(rendered).toContain("...BASE,");
|
|
552
|
+
const reparsed = parse(rendered);
|
|
553
|
+
expect(reparsed.errors).toEqual([]);
|
|
554
|
+
expect(findPattern(reparsed.patterns, "writeHandler").handlerName).toBeUndefined();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe("positional options with a top-level spread", () => {
|
|
559
|
+
const source = `
|
|
560
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
561
|
+
import { z } from "zod";
|
|
562
|
+
|
|
563
|
+
const BASE_OPTS = { description: "shared" };
|
|
564
|
+
const ADMIN = { roles: ["Admin"] };
|
|
565
|
+
|
|
566
|
+
defineFeature("f", (r) => {
|
|
567
|
+
r.writeHandler("x", z.object({}), async () => {}, { ...BASE_OPTS, access: ADMIN });
|
|
568
|
+
});
|
|
569
|
+
`;
|
|
570
|
+
const result = parse(source);
|
|
571
|
+
|
|
572
|
+
test("kept as an opaque pattern instead of dropping the spread", () => {
|
|
573
|
+
expect(result.errors).toEqual([]);
|
|
574
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
575
|
+
expect(pattern.handlerName).toBeUndefined();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("render → parse roundtrip keeps the call verbatim", () => {
|
|
579
|
+
const rendered = renderFeatureFile({
|
|
580
|
+
featureName: result.featureName ?? "",
|
|
581
|
+
patterns: result.patterns,
|
|
582
|
+
imports: [
|
|
583
|
+
...DEFAULT_IMPORTS,
|
|
584
|
+
'const BASE_OPTS = { description: "shared" };',
|
|
585
|
+
'const ADMIN = { roles: ["Admin"] };',
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
expect(rendered).toContain("...BASE_OPTS, access: ADMIN");
|
|
589
|
+
const reparsed = parse(rendered);
|
|
590
|
+
expect(reparsed.errors).toEqual([]);
|
|
591
|
+
expect(findPattern(reparsed.patterns, "writeHandler").handlerName).toBeUndefined();
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe("inline queryHandler with outputSchema", () => {
|
|
596
|
+
const source = `
|
|
597
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
598
|
+
import { z } from "zod";
|
|
599
|
+
|
|
600
|
+
defineFeature("f", (r) => {
|
|
601
|
+
r.queryHandler({
|
|
602
|
+
name: "x",
|
|
603
|
+
schema: z.object({}),
|
|
604
|
+
handler: async () => ({}),
|
|
605
|
+
access: { roles: ["Admin"] },
|
|
606
|
+
outputSchema: z.object({ id: z.string() }),
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
`;
|
|
610
|
+
const result = parse(source);
|
|
611
|
+
|
|
612
|
+
test("kept as an opaque pattern instead of dropping outputSchema", () => {
|
|
613
|
+
expect(result.errors).toEqual([]);
|
|
614
|
+
const pattern = findPattern(result.patterns, "queryHandler");
|
|
615
|
+
expect(pattern.handlerName).toBeUndefined();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("render → parse roundtrip keeps outputSchema verbatim", () => {
|
|
619
|
+
const rendered = renderFeatureFile({
|
|
620
|
+
featureName: result.featureName ?? "",
|
|
621
|
+
patterns: result.patterns,
|
|
622
|
+
imports: [...DEFAULT_IMPORTS],
|
|
623
|
+
});
|
|
624
|
+
expect(rendered).toContain("outputSchema: z.object({ id: z.string() }),");
|
|
625
|
+
const reparsed = parse(rendered);
|
|
626
|
+
expect(reparsed.errors).toEqual([]);
|
|
627
|
+
expect(findPattern(reparsed.patterns, "queryHandler").handlerName).toBeUndefined();
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe("inline writeHandler with a perform pipeline", () => {
|
|
632
|
+
const source = `
|
|
633
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
634
|
+
import { z } from "zod";
|
|
635
|
+
import { stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
636
|
+
|
|
637
|
+
defineFeature("f", (r) => {
|
|
638
|
+
r.writeHandler({
|
|
639
|
+
name: "x",
|
|
640
|
+
schema: z.object({}),
|
|
641
|
+
handler: async () => {},
|
|
642
|
+
access: { roles: ["Admin"] },
|
|
643
|
+
perform: stepsPipeline([]),
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
`;
|
|
647
|
+
const result = parse(source);
|
|
648
|
+
|
|
649
|
+
test("kept as an opaque pattern instead of dropping perform", () => {
|
|
650
|
+
expect(result.errors).toEqual([]);
|
|
651
|
+
const pattern = findPattern(result.patterns, "writeHandler");
|
|
652
|
+
expect(pattern.handlerName).toBeUndefined();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("render → parse roundtrip keeps perform verbatim", () => {
|
|
656
|
+
const rendered = renderFeatureFile({
|
|
657
|
+
featureName: result.featureName ?? "",
|
|
658
|
+
patterns: result.patterns,
|
|
659
|
+
imports: [
|
|
660
|
+
...DEFAULT_IMPORTS,
|
|
661
|
+
'import { stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";',
|
|
662
|
+
],
|
|
663
|
+
});
|
|
664
|
+
expect(rendered).toContain("perform: stepsPipeline([]),");
|
|
665
|
+
const reparsed = parse(rendered);
|
|
666
|
+
expect(reparsed.errors).toEqual([]);
|
|
667
|
+
expect(findPattern(reparsed.patterns, "writeHandler").handlerName).toBeUndefined();
|
|
668
|
+
});
|
|
669
|
+
});
|
|
@@ -1871,19 +1871,21 @@ defineFeature("f", (r) => {
|
|
|
1871
1871
|
expect(result.patterns[0]).not.toHaveProperty("agent");
|
|
1872
1872
|
});
|
|
1873
1873
|
|
|
1874
|
-
test("escapeHatch is
|
|
1874
|
+
test("escapeHatch is kept on StreamHandlerPattern (StreamHandlerDef carries it)", () => {
|
|
1875
1875
|
const result = parseInline(`
|
|
1876
1876
|
defineFeature("f", (r) => {
|
|
1877
1877
|
r.streamHandler({
|
|
1878
1878
|
name: "chat:complete",
|
|
1879
1879
|
schema: z.object({ prompt: z.string() }),
|
|
1880
1880
|
handler: async function* (input, ctx) { yield "token"; },
|
|
1881
|
-
escapeHatch: { reason: "
|
|
1881
|
+
escapeHatch: { reason: "Kept for streamHandler." },
|
|
1882
1882
|
});
|
|
1883
1883
|
});
|
|
1884
1884
|
`);
|
|
1885
1885
|
|
|
1886
|
-
expect(result.patterns[0]).
|
|
1886
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1887
|
+
escapeHatch: { reason: "Kept for streamHandler." },
|
|
1888
|
+
});
|
|
1887
1889
|
});
|
|
1888
1890
|
});
|
|
1889
1891
|
|