@cosmicdrift/kumiko-bundled-features 0.225.0 → 0.226.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.225.0",
3
+ "version": "0.226.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -130,12 +130,12 @@
130
130
  "./workflow-runner": "./src/workflow-runner/index.ts"
131
131
  },
132
132
  "dependencies": {
133
- "@cosmicdrift/kumiko-dispatcher-live": "0.225.0",
134
- "@cosmicdrift/kumiko-framework": "0.225.0",
135
- "@cosmicdrift/kumiko-headless": "0.225.0",
136
- "@cosmicdrift/kumiko-renderer": "0.225.0",
137
- "@cosmicdrift/kumiko-renderer-web": "0.225.0",
138
- "@cosmicdrift/kumiko-types": "0.225.0",
133
+ "@cosmicdrift/kumiko-dispatcher-live": "0.226.0",
134
+ "@cosmicdrift/kumiko-framework": "0.226.0",
135
+ "@cosmicdrift/kumiko-headless": "0.226.0",
136
+ "@cosmicdrift/kumiko-renderer": "0.226.0",
137
+ "@cosmicdrift/kumiko-renderer-web": "0.226.0",
138
+ "@cosmicdrift/kumiko-types": "0.226.0",
139
139
  "@mollie/api-client": "^4.5.0",
140
140
  "@node-rs/argon2": "^2.0.2",
141
141
  "@types/mailparser": "^3.4.6",
@@ -164,7 +164,7 @@
164
164
  "devDependencies": {
165
165
  "@testing-library/user-event": "^14.6.1",
166
166
  "@types/qrcode": "^1.5.5",
167
- "@cosmicdrift/kumiko-locale-de": "0.225.0",
168
- "@cosmicdrift/kumiko-locale-es": "0.225.0"
167
+ "@cosmicdrift/kumiko-locale-de": "0.226.0",
168
+ "@cosmicdrift/kumiko-locale-es": "0.226.0"
169
169
  }
170
170
  }
@@ -42,6 +42,15 @@ const testCap: CapSpec = {
42
42
  usage: async (_db, tenantId) => USAGE_BY_TENANT[tenantId] ?? 0,
43
43
  };
44
44
 
45
+ // Never wired up for any tenant — proves the "not measured" state renders
46
+ // as null, not 0, all the way through the real query handler.
47
+ const unmeasuredCap: CapSpec = {
48
+ id: "unmeasured",
49
+ label: "test.cap.unmeasured",
50
+ limit: () => 10,
51
+ usage: async () => null,
52
+ };
53
+
45
54
  let stack: TestStack;
46
55
  let db: DbConnection;
47
56
 
@@ -54,7 +63,7 @@ beforeAll(async () => {
54
63
  createTenantLifecycleFeature(),
55
64
  billingFoundationFeature,
56
65
  tierEngineFeature,
57
- createCapOverviewFeature({ caps: [testCap] }),
66
+ createCapOverviewFeature({ caps: [testCap, unmeasuredCap] }),
58
67
  ],
59
68
  });
60
69
  db = stack.db;
@@ -111,24 +120,25 @@ describe("cap-overview tenant isolation", () => {
111
120
 
112
121
  test("TenantAdmin(A) without override sees exactly A's own numbers, not a sum", async () => {
113
122
  const tenantAdminA = createTestUser({ id: 90014, tenantId: TENANT_A, roles: ["TenantAdmin"] });
114
- const result = await stack.http.queryOk<{ rows: readonly { used: number }[] }>(
115
- CapOverviewQueries.capsUsage,
116
- {},
117
- tenantAdminA,
118
- );
119
- expect(result.rows).toHaveLength(1);
120
- expect(result.rows[0]?.used).toBe(USAGE_BY_TENANT[TENANT_A]);
123
+ const result = await stack.http.queryOk<{
124
+ rows: readonly { id: string; used: number | null; percent: number | null }[];
125
+ }>(CapOverviewQueries.capsUsage, {}, tenantAdminA);
126
+ expect(result.rows).toHaveLength(2);
127
+ const widgetsRow = result.rows.find((row) => row.id === testCap.id);
128
+ expect(widgetsRow?.used).toBe(USAGE_BY_TENANT[TENANT_A]);
129
+
130
+ const unmeasuredRow = result.rows.find((row) => row.id === unmeasuredCap.id);
131
+ expect(unmeasuredRow?.used).toBeNull();
132
+ expect(unmeasuredRow?.percent).toBeNull();
121
133
  });
122
134
 
123
135
  test("SystemAdmin can read tenant B's usage via override", async () => {
124
136
  const sysAdmin = createTestUser({ id: 90015, tenantId: TENANT_A, roles: ["SystemAdmin"] });
125
- const result = await stack.http.queryOk<{ rows: readonly { used: number }[] }>(
126
- CapOverviewQueries.capsUsage,
127
- { tenantId: TENANT_B },
128
- sysAdmin,
129
- );
130
- expect(result.rows).toHaveLength(1);
131
- expect(result.rows[0]?.used).toBe(USAGE_BY_TENANT[TENANT_B]);
137
+ const result = await stack.http.queryOk<{
138
+ rows: readonly { id: string; used: number | null }[];
139
+ }>(CapOverviewQueries.capsUsage, { tenantId: TENANT_B }, sysAdmin);
140
+ const widgetsRow = result.rows.find((row) => row.id === testCap.id);
141
+ expect(widgetsRow?.used).toBe(USAGE_BY_TENANT[TENANT_B]);
132
142
  });
133
143
 
134
144
  test("SystemAdmin sees both tenants on the list, each with its own numbers", async () => {
@@ -65,6 +65,19 @@ export function createCapsUsageQuery(caps: readonly CapSpec[]): QueryHandlerDefi
65
65
  caps.map(async (cap) => {
66
66
  const used = await cap.usage(db, targetTenantId);
67
67
  const limit = cap.limit(tier);
68
+ if (used === null) {
69
+ return {
70
+ id: cap.id,
71
+ label: cap.label,
72
+ used: null,
73
+ limit,
74
+ fraction: 0,
75
+ tone: "default" as const,
76
+ percent: null,
77
+ ...(cap.icon !== undefined && { icon: cap.icon }),
78
+ ...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
79
+ };
80
+ }
68
81
  const fraction = computeFraction(used, limit);
69
82
  return {
70
83
  id: cap.id,
@@ -182,12 +182,12 @@ export function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: re
182
182
  // N+1 avoidance: usage is computed only for this page's tenant ids,
183
183
  // never for the full tenant set.
184
184
  const pageTenantIds = page.map((row) => row.tenantId);
185
- const usageByCap = new Map<string, Map<string, number>>();
185
+ const usageByCap = new Map<string, Map<string, number | null>>();
186
186
  for (const cap of listedCaps) {
187
187
  if (cap.usageBatch) {
188
188
  usageByCap.set(cap.id, await cap.usageBatch(db, pageTenantIds));
189
189
  } else {
190
- const perTenant = new Map<string, number>();
190
+ const perTenant = new Map<string, number | null>();
191
191
  for (const tenantId of pageTenantIds) {
192
192
  perTenant.set(tenantId, await cap.usage(db, tenantId));
193
193
  }
@@ -198,9 +198,15 @@ export function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: re
198
198
  const rows: TenantCapsListRow[] = page.map((row) => {
199
199
  const capFields: Record<string, CapUsage> = {};
200
200
  for (const cap of listedCaps) {
201
- const used = usageByCap.get(cap.id)?.get(row.tenantId) ?? 0;
201
+ // A tenant absent from the map still means 0 (existing behavior);
202
+ // only an explicit `null` value means "not measured".
203
+ const rawUsed = usageByCap.get(cap.id)?.get(row.tenantId);
204
+ const used = rawUsed === undefined ? 0 : rawUsed;
202
205
  const limit = cap.limit(row.tier);
203
- capFields[capFieldName(cap.id)] = { used, limit, fraction: computeFraction(used, limit) };
206
+ capFields[capFieldName(cap.id)] =
207
+ used === null
208
+ ? { used: null, limit, fraction: 0 }
209
+ : { used, limit, fraction: computeFraction(used, limit) };
204
210
  }
205
211
  return {
206
212
  tenantId: row.tenantId,
@@ -12,6 +12,7 @@ export const CAP_OVERVIEW_I18N: Readonly<Record<string, LocalizedString>> = {
12
12
  "cap-overview.platform.filter.tenant": { en: "Tenant" },
13
13
  "cap-overview.cards.empty": { en: "No caps configured for this tenant." },
14
14
  "cap-overview.cards.loading": { en: "Loading usage…" },
15
+ "cap-overview.notMeasured": { en: "Not measured yet" },
15
16
  "cap-overview.errors.progressPrimitiveMissing": {
16
17
  en: "Usage bar unavailable — Progress primitive is not registered.",
17
18
  },
@@ -15,14 +15,19 @@ export type CapSpec = {
15
15
  readonly id: string;
16
16
  readonly label: string;
17
17
  readonly limit: (tier: string) => number;
18
- readonly usage: (db: TenantDb, tenantId: TenantId) => Promise<number>;
18
+ // `null` means "no measurement exists yet for this cap" (not "0 used") —
19
+ // callers that haven't wired up instrumentation return null instead of a
20
+ // number they don't actually have.
21
+ readonly usage: (db: TenantDb, tenantId: TenantId) => Promise<number | null>;
19
22
  // Batched usage lookup for the tenant-caps:list screen — called once per
20
23
  // cap with the current page's tenant ids instead of once per (cap, row).
21
- // Optional: falls back to per-row `usage()` when absent.
24
+ // Optional: falls back to per-row `usage()` when absent. A tenant absent
25
+ // from the returned map still means 0 (unchanged) — only an explicit
26
+ // `null` value means "not measured".
22
27
  readonly usageBatch?: (
23
28
  db: TenantDb,
24
29
  tenantIds: readonly TenantId[],
25
- ) => Promise<Map<TenantId, number>>;
30
+ ) => Promise<Map<TenantId, number | null>>;
26
31
  readonly unit?: "count" | "mb" | "tokens";
27
32
  /** Icon chip on the dashboard cards. Optional — omitted renders the card
28
33
  * without an icon, same as before this field existed. */
@@ -36,7 +41,9 @@ export type CapSpec = {
36
41
  export type CapUsageTone = "default" | "warn" | "danger";
37
42
 
38
43
  export type CapUsage = {
39
- readonly used: number;
44
+ // `null` means not measured yet — `fraction`/`tone` stay non-nullable
45
+ // (0 / "default") so no consumer needs null-arithmetic to render.
46
+ readonly used: number | null;
40
47
  readonly limit: number;
41
48
  readonly fraction: number;
42
49
  };
@@ -45,7 +52,7 @@ export type CapUsageWithMeta = CapUsage & {
45
52
  readonly id: string;
46
53
  readonly label: string;
47
54
  readonly tone: CapUsageTone;
48
- readonly percent: number;
55
+ readonly percent: number | null;
49
56
  readonly icon?: CapIconKey;
50
57
  readonly accentColor?: string;
51
58
  };
@@ -83,6 +83,19 @@ function capRow(overrides: Partial<CapUsageWithMeta> = {}): CapUsageWithMeta {
83
83
  };
84
84
  }
85
85
 
86
+ function nullCapRow(overrides: Partial<CapUsageWithMeta> = {}): CapUsageWithMeta {
87
+ return {
88
+ id: "cap-null",
89
+ label: "cap.label.null",
90
+ tone: "default",
91
+ percent: null,
92
+ used: null,
93
+ limit: 100,
94
+ fraction: 0,
95
+ ...overrides,
96
+ };
97
+ }
98
+
86
99
  beforeEach(() => {
87
100
  queryState = { data: { rows: [] }, loading: false, error: null };
88
101
  useQuerySpy.mockClear();
@@ -187,6 +200,39 @@ describe("CapCardsPanel", () => {
187
200
  expect("tenantId" in numericTenantIdArgs).toBe(false);
188
201
  });
189
202
 
203
+ test("a card with used: null shows the em-dash value and not-measured text, no bar or percent badge", () => {
204
+ queryState = {
205
+ data: { rows: [nullCapRow()] },
206
+ loading: false,
207
+ error: null,
208
+ };
209
+
210
+ renderPanel(undefined);
211
+
212
+ expect(screen.getByTestId("cap-card").textContent).toContain("—");
213
+ expect(screen.getByTestId("cap-usage-not-measured")).toBeTruthy();
214
+ expect(screen.queryByTestId("cap-usage-bar")).toBeNull();
215
+ expect(screen.queryByText("null%")).toBeNull();
216
+ expect(screen.queryByText("0%")).toBeNull();
217
+ });
218
+
219
+ test("a null-usage card and a measured card in the same panel each render their own state", () => {
220
+ queryState = {
221
+ data: {
222
+ rows: [capRow({ id: "c1", label: "Storage", used: 3, limit: 10 }), nullCapRow()],
223
+ },
224
+ loading: false,
225
+ error: null,
226
+ };
227
+
228
+ renderPanel(undefined);
229
+
230
+ expect(screen.getAllByTestId("cap-card")).toHaveLength(2);
231
+ expect(screen.getByText("3 / 10")).toBeTruthy();
232
+ expect(screen.getByTestId("cap-usage-not-measured")).toBeTruthy();
233
+ expect(screen.getAllByTestId("cap-usage-bar")).toHaveLength(1);
234
+ });
235
+
190
236
  test("loading with empty rows shows the loading text instead of the empty state", () => {
191
237
  queryState = { data: null, loading: true, error: null };
192
238
 
@@ -23,6 +23,20 @@ function fillClass(): string | undefined {
23
23
  }
24
24
 
25
25
  describe("CapUsageBar", () => {
26
+ test("renders the not-measured text instead of a bar when used is null", () => {
27
+ const usage: CapUsage = { used: null, limit: 100, fraction: 0 };
28
+ render(
29
+ <Wrapper>
30
+ <CapUsageBar usage={usage} showLabel={false} />
31
+ </Wrapper>,
32
+ );
33
+
34
+ expect(screen.queryByTestId("cap-usage-bar")).toBeNull();
35
+ expect(screen.getByTestId("cap-usage-not-measured").textContent).toBe(
36
+ "cap-overview.notMeasured",
37
+ );
38
+ });
39
+
26
40
  test("renders the danger fill class when usage is at or over the limit", () => {
27
41
  const usage: CapUsage = { used: 120, limit: 100, fraction: 1.2 };
28
42
  render(
@@ -35,6 +35,14 @@ const CAP_ICONS: Record<CapIconKey, ComponentType<{ className?: string }>> = {
35
35
  gauge: Gauge,
36
36
  };
37
37
 
38
+ // Narrows `percent` to non-null alongside `used` — the query handler only
39
+ // ever sets both or neither, but the type doesn't encode that link.
40
+ function isMeasured(
41
+ cap: CapUsageWithMeta,
42
+ ): cap is CapUsageWithMeta & { used: number; percent: number } {
43
+ return cap.used !== null;
44
+ }
45
+
38
46
  export function CapCardsPanel({ filterParams }: ExtensionSectionProps): ReactNode {
39
47
  const t = useTranslation();
40
48
  const { Banner } = usePrimitives();
@@ -81,14 +89,16 @@ export function CapCardsPanel({ filterParams }: ExtensionSectionProps): ReactNod
81
89
  key={cap.id}
82
90
  icon={Icon !== undefined ? <Icon className="size-4" /> : undefined}
83
91
  label={t(cap.label)}
84
- value={`${cap.used} / ${cap.limit}`}
92
+ value={isMeasured(cap) ? `${cap.used} / ${cap.limit}` : "—"}
85
93
  {...(cap.accentColor !== undefined && { accentColor: cap.accentColor })}
86
94
  testId="cap-card"
87
95
  >
88
96
  <div className="flex flex-col gap-2">
89
- <div className="flex items-center justify-between gap-2">
90
- <StatusBadge tone={TONE_TO_STATUS[cap.tone]}>{`${cap.percent}%`}</StatusBadge>
91
- </div>
97
+ {isMeasured(cap) && (
98
+ <div className="flex items-center justify-between gap-2">
99
+ <StatusBadge tone={TONE_TO_STATUS[cap.tone]}>{`${cap.percent}%`}</StatusBadge>
100
+ </div>
101
+ )}
92
102
  <CapUsageBar usage={cap} showLabel={false} />
93
103
  </div>
94
104
  </StatCard>
@@ -15,6 +15,15 @@ export function CapUsageBar({
15
15
  }): ReactNode {
16
16
  const { Progress, Text, Banner } = usePrimitives();
17
17
  const t = useTranslation();
18
+
19
+ if (usage.used === null) {
20
+ return (
21
+ <Text variant="small" testId="cap-usage-not-measured">
22
+ {t("cap-overview.notMeasured")}
23
+ </Text>
24
+ );
25
+ }
26
+
18
27
  return (
19
28
  <div className="flex flex-col gap-1">
20
29
  {Progress ? (
@@ -5,10 +5,10 @@ import type { CapUsage } from "../types";
5
5
  import { CapUsageBar } from "./cap-usage-bar";
6
6
 
7
7
  function isCapUsage(value: unknown): value is CapUsage {
8
+ if (typeof value !== "object" || value === null) return false;
9
+ const used = (value as { used?: unknown }).used;
8
10
  return (
9
- typeof value === "object" &&
10
- value !== null &&
11
- typeof (value as { used?: unknown }).used === "number" &&
11
+ (typeof used === "number" || used === null) &&
12
12
  typeof (value as { limit?: unknown }).limit === "number" &&
13
13
  typeof (value as { fraction?: unknown }).fraction === "number"
14
14
  );
@@ -244,12 +244,17 @@ describe("form-draft cleanup job — FileRef release (#1915)", () => {
244
244
 
245
245
  await dispatchCleanup();
246
246
 
247
- await waitFor(async () => {
248
- expect(await draftExists("wizard:stale-with-photo")).toBe(false);
247
+ // Draft deletion and FileRef release are two separate awaited phases in
248
+ // cleanup.job.ts — the whole batch is deleted first, then released row
249
+ // by row. Waiting on draftExists only proves the delete phase finished,
250
+ // not the (real DB + provider) release; wait on the actual release
251
+ // effect instead, or this assertion races the still-in-flight release.
252
+ await waitFor(() => {
253
+ expect(providerA.keys()).not.toContain(key);
249
254
  });
255
+ expect(await draftExists("wizard:stale-with-photo")).toBe(false);
250
256
  expect(await draftExists("wizard:stale-no-photo")).toBe(false);
251
257
  expect(await draftExists("wizard:fresh")).toBe(true);
252
- expect(providerA.keys()).not.toContain(key);
253
258
  });
254
259
 
255
260
  test("resolves each stale row's own tenant provider — never the job's SYSTEM_TENANT_ID run-tenant, never another row's tenant", async () => {
@@ -267,15 +272,18 @@ describe("form-draft cleanup job — FileRef release (#1915)", () => {
267
272
 
268
273
  await dispatchCleanup();
269
274
 
270
- await waitFor(async () => {
271
- expect(await draftExists("wizard:tenant-a-stale")).toBe(false);
275
+ // Same two-phase timing as the single-tenant release test above: wait on
276
+ // each provider's actual release, not on draft deletion (which finishes
277
+ // for the whole batch before either row's release even starts).
278
+ await waitFor(() => {
279
+ expect(providerA.keys()).not.toContain(keyA);
272
280
  });
273
- await waitFor(async () => {
274
- expect(await draftExists("wizard:tenant-b-stale")).toBe(false);
281
+ await waitFor(() => {
282
+ expect(providerB.keys()).not.toContain(keyB);
275
283
  });
276
284
 
277
- expect(providerA.keys()).not.toContain(keyA);
278
- expect(providerB.keys()).not.toContain(keyB);
285
+ expect(await draftExists("wizard:tenant-a-stale")).toBe(false);
286
+ expect(await draftExists("wizard:tenant-b-stale")).toBe(false);
279
287
  // Cross-tenant guard: neither key ever lands in the other tenant's provider.
280
288
  expect(providerA.keys()).not.toContain(keyB);
281
289
  expect(providerB.keys()).not.toContain(keyA);
@@ -286,15 +294,28 @@ describe("form-draft cleanup job — FileRef release (#1915)", () => {
286
294
  await providerA.write(forgedKey, new Uint8Array([9, 9, 9]), "image/jpeg");
287
295
  // Deliberately no seedFileRef — the stale draft's `values` claims a
288
296
  // storageKey its owner never actually uploaded.
289
-
290
297
  await saveDraft("wizard:stale-forged", { photo: fileRefPointer(forgedKey) });
291
- await backdate("wizard:stale-forged", 31);
298
+ await backdate("wizard:stale-forged", 40);
299
+
300
+ // Release sentinel: a real, legitimately-owned FileRef backdated less
301
+ // than the forged row, so selectStaleDraftsBatch's oldest-first order
302
+ // processes the forged row first. Releases are awaited sequentially in
303
+ // order within one job run, so observing the sentinel's release proves
304
+ // the forged row's (earlier) release turn already ran and correctly did
305
+ // nothing — a plain draftExists wait can't prove that, only that the
306
+ // delete phase (which precedes all releases) has finished.
307
+ const sentinelKey = "tenant-a/vehicle/forged-sentinel.jpg";
308
+ await providerA.write(sentinelKey, new Uint8Array([7]), "image/jpeg");
309
+ await seedFileRef(sentinelKey);
310
+ await saveDraft("wizard:stale-forged-sentinel", { photo: fileRefPointer(sentinelKey) });
311
+ await backdate("wizard:stale-forged-sentinel", 31);
292
312
 
293
313
  await dispatchCleanup();
294
314
 
295
- await waitFor(async () => {
296
- expect(await draftExists("wizard:stale-forged")).toBe(false);
315
+ await waitFor(() => {
316
+ expect(providerA.keys()).not.toContain(sentinelKey);
297
317
  });
318
+ expect(await draftExists("wizard:stale-forged")).toBe(false);
298
319
  expect(providerA.keys()).toContain(forgedKey);
299
320
  });
300
321
 
@@ -304,18 +325,28 @@ describe("form-draft cleanup job — FileRef release (#1915)", () => {
304
325
  await seedFileRef(key);
305
326
  // The photo was uploaded when the domain entity was originally created —
306
327
  // long before this editing session's draft. backdate() (below) also
307
- // ages the draft row to 31 days, so the file must be aged further still
328
+ // ages the draft row to 40 days, so the file must be aged further still
308
329
  // to model "predates the draft" rather than "same moment".
309
330
  await backdateFileRef(key, 60);
310
331
 
311
332
  await saveDraft("wizard:stale-with-prefilled-photo", { photo: fileRefPointer(key) });
312
- await backdate("wizard:stale-with-prefilled-photo", 31);
333
+ await backdate("wizard:stale-with-prefilled-photo", 40);
334
+
335
+ // Release sentinel — see the forged-draft-value test above for why a
336
+ // plain draftExists wait can't prove this row's release was actually
337
+ // (correctly) skipped, only that it hasn't been attempted yet.
338
+ const sentinelKey = "tenant-a/vehicle/prefilled-sentinel.jpg";
339
+ await providerA.write(sentinelKey, new Uint8Array([7]), "image/jpeg");
340
+ await seedFileRef(sentinelKey);
341
+ await saveDraft("wizard:stale-with-prefilled-sentinel", { photo: fileRefPointer(sentinelKey) });
342
+ await backdate("wizard:stale-with-prefilled-sentinel", 31);
313
343
 
314
344
  await dispatchCleanup();
315
345
 
316
- await waitFor(async () => {
317
- expect(await draftExists("wizard:stale-with-prefilled-photo")).toBe(false);
346
+ await waitFor(() => {
347
+ expect(providerA.keys()).not.toContain(sentinelKey);
318
348
  });
349
+ expect(await draftExists("wizard:stale-with-prefilled-photo")).toBe(false);
319
350
  // The live domain entity this draft was editing still references `key`.
320
351
  expect(providerA.keys()).toContain(key);
321
352
  });
@@ -336,9 +367,9 @@ describe("form-draft cleanup job — FileRef release (#1915)", () => {
336
367
 
337
368
  await dispatchCleanup();
338
369
 
339
- await waitFor(async () => {
340
- expect(await draftExists("wizard:new:create-mode-stale")).toBe(false);
370
+ await waitFor(() => {
371
+ expect(providerA.keys()).not.toContain(key);
341
372
  });
342
- expect(providerA.keys()).not.toContain(key);
373
+ expect(await draftExists("wizard:new:create-mode-stale")).toBe(false);
343
374
  });
344
375
  });
@@ -131,12 +131,12 @@ describe("NotesSection — entry display", () => {
131
131
  test("note body and meta line render as two distinct nodes, not one run-on line", async () => {
132
132
  const view = renderSection();
133
133
  await waitFor(() => {
134
- if (view.queryByTestId("notes-section-row-note-1-body") === null) {
134
+ if (view.queryByTestId("notes-section-body-note-1") === null) {
135
135
  throw new Error("row not rendered yet");
136
136
  }
137
137
  });
138
- const bodyEl = view.getByTestId("notes-section-row-note-1-body");
139
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
138
+ const bodyEl = view.getByTestId("notes-section-body-note-1");
139
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
140
140
  expect(bodyEl).not.toBe(metaEl);
141
141
  expect(bodyEl.textContent).toBe(NOTE_BODY);
142
142
  expect(metaEl.textContent).not.toContain(NOTE_BODY);
@@ -145,14 +145,25 @@ describe("NotesSection — entry display", () => {
145
145
  expect(row.className).toContain("flex-col");
146
146
  });
147
147
 
148
+ test("the row testid prefix matches exactly one node per note, not three", async () => {
149
+ const view = renderSection();
150
+ await waitFor(() => {
151
+ if (view.queryByTestId("notes-section-row-note-1") === null) {
152
+ throw new Error("rows not rendered yet");
153
+ }
154
+ });
155
+ const matches = view.container.querySelectorAll('[data-testid^="notes-section-row-"]');
156
+ expect(matches.length).toBe(NOTE_ROWS.length);
157
+ });
158
+
148
159
  test("a stamped authorName renders as the author, never the raw authorId", async () => {
149
160
  const view = renderSection();
150
161
  await waitFor(() => {
151
- if (view.queryByTestId("notes-section-row-note-1-meta") === null) {
162
+ if (view.queryByTestId("notes-section-meta-note-1") === null) {
152
163
  throw new Error("row not rendered yet");
153
164
  }
154
165
  });
155
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
166
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
156
167
  expect(metaEl.textContent).not.toContain(AUTHOR_ID);
157
168
  expect(metaEl.textContent).toContain(AUTHOR_NAME);
158
169
  });
@@ -160,22 +171,22 @@ describe("NotesSection — entry display", () => {
160
171
  test("a null authorName (legacy row) renders the placeholder", async () => {
161
172
  const view = renderSection();
162
173
  await waitFor(() => {
163
- if (view.queryByTestId("notes-section-row-note-2-meta") === null) {
174
+ if (view.queryByTestId("notes-section-meta-note-2") === null) {
164
175
  throw new Error("row not rendered yet");
165
176
  }
166
177
  });
167
- const metaEl = view.getByTestId("notes-section-row-note-2-meta");
178
+ const metaEl = view.getByTestId("notes-section-meta-note-2");
168
179
  expect(metaEl.textContent).toContain("Unknown author");
169
180
  });
170
181
 
171
182
  test("a shredded authorName sentinel renders the placeholder, never [[erased]]", async () => {
172
183
  const view = renderSection();
173
184
  await waitFor(() => {
174
- if (view.queryByTestId("notes-section-row-note-3-meta") === null) {
185
+ if (view.queryByTestId("notes-section-meta-note-3") === null) {
175
186
  throw new Error("row not rendered yet");
176
187
  }
177
188
  });
178
- const metaEl = view.getByTestId("notes-section-row-note-3-meta");
189
+ const metaEl = view.getByTestId("notes-section-meta-note-3");
179
190
  expect(metaEl.textContent).toContain("Unknown author");
180
191
  expect(metaEl.textContent).not.toContain(PII_ERASED_SENTINEL);
181
192
  });
@@ -183,11 +194,11 @@ describe("NotesSection — entry display", () => {
183
194
  test("date renders formatted, never the raw ISO timestamp", async () => {
184
195
  const view = renderSection();
185
196
  await waitFor(() => {
186
- if (view.queryByTestId("notes-section-row-note-1-meta") === null) {
197
+ if (view.queryByTestId("notes-section-meta-note-1") === null) {
187
198
  throw new Error("row not rendered yet");
188
199
  }
189
200
  });
190
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
201
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
191
202
  expect(metaEl.textContent).not.toContain(INSERTED_AT);
192
203
  expect(metaEl.textContent).toContain(formatWhen(INSERTED_AT));
193
204
  });
@@ -149,8 +149,8 @@ export function NotesSection({
149
149
  data-testid={`notes-section-row-${n.id}`}
150
150
  className="flex flex-col gap-1 rounded-md border p-2"
151
151
  >
152
- <Text testId={`notes-section-row-${n.id}-body`}>{n.body}</Text>
153
- <Text variant="small" testId={`notes-section-row-${n.id}-meta`}>
152
+ <Text testId={`notes-section-body-${n.id}`}>{n.body}</Text>
153
+ <Text variant="small" testId={`notes-section-meta-${n.id}`}>
154
154
  {t("notesHistory.section.meta", {
155
155
  author: displayAuthorName(n.authorName, t("notesHistory.section.authorUnknown")),
156
156
  date: formatWhen(n.insertedAt),
@@ -6,10 +6,21 @@ import { userSessionTable } from "../schema/user-session";
6
6
 
7
7
  // Admin single-session inspector — mirrors list.query's decrypt handling for
8
8
  // the one-row case. ctx.db (TenantDb) applies tenant-scoping automatically.
9
+ const sessionRowSchema = z.object({
10
+ id: z.string(),
11
+ userId: z.string(),
12
+ createdAt: z.unknown(),
13
+ expiresAt: z.unknown(),
14
+ revokedAt: z.unknown(),
15
+ ip: z.string().nullable(),
16
+ userAgent: z.string().nullable(),
17
+ });
18
+
9
19
  export const detailQuery = defineQueryHandler({
10
20
  name: "user-session:detail",
11
21
  schema: z.object({ id: z.uuid() }),
12
22
  access: { roles: access.admin },
23
+ outputSchema: sessionRowSchema.nullable(),
13
24
  handler: async (query, ctx) => {
14
25
  const row = await fetchOne<{
15
26
  id: string;
@@ -33,6 +33,20 @@ export const listQuery = definePagedQueryHandler({
33
33
  sortDirection: z.enum(["asc", "desc"]).optional(),
34
34
  }),
35
35
  access: { roles: access.admin },
36
+ outputSchema: z.object({
37
+ rows: z.array(
38
+ z.object({
39
+ id: z.string(),
40
+ userId: z.string(),
41
+ createdAt: z.unknown(),
42
+ expiresAt: z.unknown(),
43
+ revokedAt: z.unknown(),
44
+ ip: z.string().nullable(),
45
+ userAgent: z.string().nullable(),
46
+ }),
47
+ ),
48
+ nextCursor: z.string().nullable(),
49
+ }),
36
50
  handler: async (query, ctx) => {
37
51
  const requestedSort = query.payload.sort;
38
52
  const sortColumn: SortableColumn =