@cosmicdrift/kumiko-framework 0.220.0 → 0.220.1

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-framework",
3
- "version": "0.220.0",
3
+ "version": "0.220.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -194,7 +194,7 @@
194
194
  "./package.json": "./package.json"
195
195
  },
196
196
  "dependencies": {
197
- "@cosmicdrift/kumiko-types": "0.220.0",
197
+ "@cosmicdrift/kumiko-types": "0.220.1",
198
198
  "bullmq": "^5.76.7",
199
199
  "bun-types": "^1.3.13",
200
200
  "hono": "^4.13.1",
@@ -210,7 +210,7 @@
210
210
  "zod": "^4.4.3"
211
211
  },
212
212
  "devDependencies": {
213
- "@cosmicdrift/kumiko-dispatcher-live": "0.220.0",
213
+ "@cosmicdrift/kumiko-dispatcher-live": "0.220.1",
214
214
  "bun-types": "^1.3.13",
215
215
  "pino-pretty": "^13.1.3"
216
216
  },
@@ -85,7 +85,7 @@ describe("ctx.locale falls back to the app's boot-configured defaultLocale", ()
85
85
  beforeAll(async () => {
86
86
  stack = await setupTestStack({
87
87
  features: [localeProbe],
88
- extraContext: { defaultLocale: "ja" },
88
+ defaultLocale: "ja",
89
89
  });
90
90
  });
91
91
 
@@ -108,7 +108,7 @@ describe("ctx.locale falls back to SessionUser.locale ahead of the boot default"
108
108
  beforeAll(async () => {
109
109
  stack = await setupTestStack({
110
110
  features: [localeProbe],
111
- extraContext: { defaultLocale: "ja" },
111
+ defaultLocale: "ja",
112
112
  });
113
113
  });
114
114
 
@@ -38,7 +38,7 @@ const executor = createEventStoreExecutor(sharedWidgetTable, sharedWidgetEntity,
38
38
  entityName: "widget",
39
39
  });
40
40
 
41
- const deliveryTimes: number[] = [];
41
+ let deliveryCount = 0;
42
42
 
43
43
  const listenFeature = defineFeature("listen", (r) => {
44
44
  r.entity("widget", sharedWidgetEntity);
@@ -47,7 +47,7 @@ const listenFeature = defineFeature("listen", (r) => {
47
47
  name: "latency-probe",
48
48
  apply: {
49
49
  "widget.created": async () => {
50
- deliveryTimes.push(Date.now());
50
+ deliveryCount += 1;
51
51
  },
52
52
  },
53
53
  });
@@ -79,17 +79,17 @@ afterAll(async () => {
79
79
 
80
80
  describe("E.4 — PG NOTIFY/LISTEN wake-up", () => {
81
81
  test("NOTIFY on commit triggers runOnce without waiting for the poll timer", async () => {
82
- deliveryTimes.length = 0;
82
+ deliveryCount = 0;
83
83
 
84
84
  await stack.eventDispatcher?.start();
85
85
  try {
86
86
  await executor.create({ name: "latency-test" }, admin, tdb);
87
87
 
88
88
  const deadline = Date.now() + 5000;
89
- while (Date.now() < deadline && deliveryTimes.length === 0) {
89
+ while (Date.now() < deadline && deliveryCount === 0) {
90
90
  await new Promise((r) => setTimeout(r, 5));
91
91
  }
92
- expect(deliveryTimes).toHaveLength(1);
92
+ expect(deliveryCount).toBe(1);
93
93
  } finally {
94
94
  await stack.eventDispatcher?.stop();
95
95
  }
@@ -105,16 +105,16 @@ describe("E.4 — PG NOTIFY/LISTEN wake-up", () => {
105
105
  await stack.eventDispatcher?.stop();
106
106
  }
107
107
 
108
- deliveryTimes.length = 0;
108
+ deliveryCount = 0;
109
109
  await stack.eventDispatcher?.start();
110
110
  try {
111
111
  await executor.create({ name: "restart-probe" }, admin, tdb);
112
112
 
113
113
  const deadline = Date.now() + 5000;
114
- while (Date.now() < deadline && deliveryTimes.length === 0) {
114
+ while (Date.now() < deadline && deliveryCount === 0) {
115
115
  await new Promise((r) => setTimeout(r, 5));
116
116
  }
117
- expect(deliveryTimes).toHaveLength(1);
117
+ expect(deliveryCount).toBe(1);
118
118
  } finally {
119
119
  await stack.eventDispatcher?.stop();
120
120
  }
@@ -190,10 +190,12 @@ describe("ctx.loadAggregate via queryHandler — Marten AggregateStreamAsync equ
190
190
  { customer: "TimeTraveler", status: "draft" },
191
191
  admin,
192
192
  );
193
- // Capture the event timestamp from the events-table for a precise cutoff.
194
- // A too-small offset risks clock granularity flakes we rely on the
195
- // millisecond-precision timestamp column.
196
- const preApprove = new Date();
193
+ // Cutoff must come from the event row's created_at (Postgres now()), not
194
+ // host wall-clock Docker clock skew made Date()-based cutoffs exclude
195
+ // the create event (fw#2425).
196
+ const beforeApprove = await loadAggregateRaw(stack.db, created.id, admin.tenantId);
197
+ expect(beforeApprove).toHaveLength(1);
198
+ const preApprove = beforeApprove[0]!.createdAt;
197
199
  // Make sure the next event's createdAt is strictly after the cutoff.
198
200
  await new Promise((r) => setTimeout(r, 10));
199
201
  await stack.http.writeOk(
@@ -210,10 +212,10 @@ describe("ctx.loadAggregate via queryHandler — Marten AggregateStreamAsync equ
210
212
  );
211
213
  expect(now.approved).toBe(true);
212
214
 
213
- // asOf preApprove: the approval is in the future, not yet visible.
215
+ // asOf create.createdAt: the approval is in the future, not yet visible.
214
216
  const past = await stack.http.queryOk<{ approved: boolean; status: string }>(
215
217
  "asoftest:query:invoice:state",
216
- { id: created.id, asOf: preApprove.toISOString() },
218
+ { id: created.id, asOf: preApprove.toString() },
217
219
  admin,
218
220
  );
219
221
  expect(past.approved).toBe(false);
@@ -204,15 +204,22 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
204
204
  }
205
205
 
206
206
  if (subjectProps.length > 1) {
207
- report(
208
- subjectProps[0]!.prop,
209
- `multiple subject annotations on one field (${subjectProps.map((s) => s.name).join(", ")}) — needs a human decision on which subject is correct`,
210
- );
207
+ const first = subjectProps[0];
208
+ if (first) {
209
+ report(
210
+ first.prop,
211
+ `multiple subject annotations on one field (${subjectProps.map((s) => s.name).join(", ")}) — needs a human decision on which subject is correct`,
212
+ );
213
+ }
211
214
  // skip: reported above — multiple subject annotations need a human call on which one wins
212
215
  return;
213
216
  }
214
217
 
215
- const subject = subjectProps[0]!;
218
+ const subject = subjectProps[0];
219
+ if (!subject) {
220
+ // skip: length===0 and length>1 already returned — empty [0] is unreachable
221
+ return;
222
+ }
216
223
 
217
224
  if (NO_PERSONAL_FACTORIES.has(factoryName)) {
218
225
  report(
@@ -276,12 +283,13 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
276
283
  return;
277
284
  }
278
285
  const ownerFieldProp = findProp(init, "ownerField");
279
- if (!ownerFieldProp?.getInitializer()) {
286
+ const ownerFieldInit = ownerFieldProp?.getInitializer();
287
+ if (!ownerFieldInit) {
280
288
  report(subject.prop, 'userOwned is missing an "ownerField" property');
281
289
  // skip: reported above — userOwned is missing its ownerField property
282
290
  return;
283
291
  }
284
- personalInit = `{ of: ${ownerFieldProp.getInitializer()!.getText()} }`;
292
+ personalInit = `{ of: ${ownerFieldInit.getText()} }`;
285
293
  }
286
294
 
287
295
  const subjectIsRefOrPlaintext =
@@ -331,9 +339,9 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
331
339
  const hasSearchable = !!searchableProp;
332
340
  const hasSensitive = !!sensitiveProp;
333
341
 
334
- if (hasSensitive && (hasLookupable || hasSearchable)) {
342
+ if (sensitiveProp && (hasLookupable || hasSearchable)) {
335
343
  report(
336
- sensitiveProp!,
344
+ sensitiveProp,
337
345
  'sensitive combined with lookupable/searchable — two find values ("secret" vs "exact"/"fuzzy"), needs a human decision',
338
346
  );
339
347
  // skip: reported above — sensitive plus lookupable/searchable is an ambiguous find value
@@ -348,10 +356,13 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
348
356
  else find = "none";
349
357
 
350
358
  if (isLongTextFind && (find === "exact" || find === "fuzzy")) {
351
- report(
352
- (lookupableProp ?? searchableProp)!,
353
- 'lookupable/searchable on createLongTextField — only "none"/"secret" are valid find values on longText',
354
- );
359
+ const findSite = lookupableProp ?? searchableProp;
360
+ if (findSite) {
361
+ report(
362
+ findSite,
363
+ 'lookupable/searchable on createLongTextField — only "none"/"secret" are valid find values on longText',
364
+ );
365
+ }
355
366
  // skip: reported above — exact/fuzzy find isn't valid on createLongTextField
356
367
  return;
357
368
  }
@@ -381,7 +392,12 @@ function applyTransform(
381
392
  next: { personal: string; reason: string | undefined; find: string | undefined },
382
393
  ): void {
383
394
  const properties = obj.getProperties();
384
- const anchor = removedProps[0]!;
395
+ // Call sites always pass at least subject.prop; without an anchor there is nowhere to insert.
396
+ const anchor = removedProps[0];
397
+ if (!anchor) {
398
+ // skip: no removed prop to anchor inserts on — nothing to transform
399
+ return;
400
+ }
385
401
  const anchorIndex = properties.indexOf(anchor);
386
402
  const removedSet = new Set<PropertyAssignment>(removedProps);
387
403
  let insertIndex = 0;
@@ -72,6 +72,8 @@ export type TestStackOptions = {
72
72
  jwtSecret?: string;
73
73
  /** Extra fields merged into the AppContext (e.g. _notifyFactory, configResolver).
74
74
  * Can be a function receiving (registry, db, sseBroker) for late binding. */
75
+ /** Boot default locale (BCP-47) merged into AppContext before extraContext. */
76
+ defaultLocale?: string;
75
77
  extraContext?:
76
78
  | Record<string, unknown>
77
79
  | ((deps: {
@@ -294,6 +296,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
294
296
  registry,
295
297
  ...(options.masterKeyProvider ? { masterKeyProvider: options.masterKeyProvider } : {}),
296
298
  ...(fileProviderResolver ? { _fileProviderResolver: fileProviderResolver } : {}),
299
+ ...(options.defaultLocale !== undefined && { defaultLocale: options.defaultLocale }),
297
300
  ...(typeof options.extraContext === "function"
298
301
  ? options.extraContext({ registry, db: testDb.db, sseBroker, redis: testRedis.redis })
299
302
  : options.extraContext),
@@ -325,9 +325,11 @@ async function applyCodemods(
325
325
  return 0;
326
326
  }
327
327
 
328
+ const firstPending = pending[0];
329
+ if (!firstPending) return 0;
328
330
  const latestVersion = pending.reduce(
329
331
  (max, e) => (compareVersions(e.version, max) > 0 ? e.version : max),
330
- pending[0]!.version,
332
+ firstPending.version,
331
333
  );
332
334
  writeUpgradeMarker(targetDir, {
333
335
  version: latestVersion,