@substrat-run/contract-tests 0.159.0 → 0.161.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.
Files changed (75) hide show
  1. package/dist/attachment-bytes-suite.d.ts +9 -0
  2. package/dist/attachment-bytes-suite.d.ts.map +1 -0
  3. package/dist/attachment-bytes-suite.js +97 -0
  4. package/dist/attachment-bytes-suite.js.map +1 -0
  5. package/dist/connector-fixture.d.ts +2 -2
  6. package/dist/connector-fixture.d.ts.map +1 -1
  7. package/dist/directory-audit-suite.d.ts +23 -0
  8. package/dist/directory-audit-suite.d.ts.map +1 -0
  9. package/dist/directory-audit-suite.js +560 -0
  10. package/dist/directory-audit-suite.js.map +1 -0
  11. package/dist/directory-restore-suite.d.ts.map +1 -1
  12. package/dist/directory-restore-suite.js +99 -0
  13. package/dist/directory-restore-suite.js.map +1 -1
  14. package/dist/index.d.ts +11 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +14 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/introspection-reads-suite.d.ts +35 -0
  19. package/dist/introspection-reads-suite.d.ts.map +1 -0
  20. package/dist/introspection-reads-suite.js +114 -0
  21. package/dist/introspection-reads-suite.js.map +1 -0
  22. package/dist/many-migrations-suite.d.ts +9 -0
  23. package/dist/many-migrations-suite.d.ts.map +1 -0
  24. package/dist/many-migrations-suite.js +76 -0
  25. package/dist/many-migrations-suite.js.map +1 -0
  26. package/dist/many-migrations.d.ts +16 -0
  27. package/dist/many-migrations.d.ts.map +1 -0
  28. package/dist/many-migrations.js +50 -0
  29. package/dist/many-migrations.js.map +1 -0
  30. package/dist/membership-executor-suite.d.ts.map +1 -1
  31. package/dist/membership-executor-suite.js +8 -5
  32. package/dist/membership-executor-suite.js.map +1 -1
  33. package/dist/modules.d.ts.map +1 -1
  34. package/dist/modules.js +10 -0
  35. package/dist/modules.js.map +1 -1
  36. package/dist/reserved-role-suite.d.ts +6 -0
  37. package/dist/reserved-role-suite.d.ts.map +1 -1
  38. package/dist/reserved-role-suite.js +176 -8
  39. package/dist/reserved-role-suite.js.map +1 -1
  40. package/dist/restore-role-suite.d.ts +17 -0
  41. package/dist/restore-role-suite.d.ts.map +1 -0
  42. package/dist/restore-role-suite.js +107 -0
  43. package/dist/restore-role-suite.js.map +1 -0
  44. package/dist/role-state-suite.d.ts +17 -0
  45. package/dist/role-state-suite.d.ts.map +1 -0
  46. package/dist/role-state-suite.js +80 -0
  47. package/dist/role-state-suite.js.map +1 -0
  48. package/dist/schedule-platform-requests-suite.d.ts +7 -1
  49. package/dist/schedule-platform-requests-suite.d.ts.map +1 -1
  50. package/dist/schedule-platform-requests-suite.js +349 -2
  51. package/dist/schedule-platform-requests-suite.js.map +1 -1
  52. package/dist/schedule-purge-request-module.d.ts +88 -0
  53. package/dist/schedule-purge-request-module.d.ts.map +1 -0
  54. package/dist/schedule-purge-request-module.js +114 -0
  55. package/dist/schedule-purge-request-module.js.map +1 -0
  56. package/dist/schedule-purge-two-module.d.ts +141 -0
  57. package/dist/schedule-purge-two-module.d.ts.map +1 -0
  58. package/dist/schedule-purge-two-module.js +153 -0
  59. package/dist/schedule-purge-two-module.js.map +1 -0
  60. package/dist/scope-host-suite.d.ts.map +1 -1
  61. package/dist/scope-host-suite.js +1329 -8
  62. package/dist/scope-host-suite.js.map +1 -1
  63. package/dist/size-reads-suite.d.ts +20 -0
  64. package/dist/size-reads-suite.d.ts.map +1 -0
  65. package/dist/size-reads-suite.js +77 -0
  66. package/dist/size-reads-suite.js.map +1 -0
  67. package/dist/stored-vertical-copy-suite.d.ts +15 -0
  68. package/dist/stored-vertical-copy-suite.d.ts.map +1 -0
  69. package/dist/stored-vertical-copy-suite.js +68 -0
  70. package/dist/stored-vertical-copy-suite.js.map +1 -0
  71. package/dist/unreadable-scope-row-suite.d.ts +16 -0
  72. package/dist/unreadable-scope-row-suite.d.ts.map +1 -0
  73. package/dist/unreadable-scope-row-suite.js +152 -0
  74. package/dist/unreadable-scope-row-suite.js.map +1 -0
  75. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
2
  import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
3
- import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, isUndecodablePlatformRequest, platformRequestEntry, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, unheldMigrationDigest, } from '@substrat-run/contracts';
4
- import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, } from '@substrat-run/kernel';
3
+ import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, LIST_PAGE_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, isUndecodablePlatformRequest, platformRequestEntry, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, unheldMigrationDigest, withUpstreamReference, } from '@substrat-run/contracts';
4
+ import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, withheldOpsFailure, withheldStaffFailure, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, WIRE_AUTH_FILTER_TEXT_MAX, } from '@substrat-run/kernel';
5
5
  import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, TEST_LIFECYCLE, testModManifest, victimModManifest, } from './modules.js';
6
6
  import { testOperations } from './test-operations.js';
7
7
  import { derivedDigestOf, heldMigrations } from './held-migrations.js';
@@ -268,6 +268,62 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
268
268
  scopeId: scopeId.parse(ulid()),
269
269
  }), 'conflict');
270
270
  });
271
+ describe('provisioning takes a vertical slug and a non-empty kind and name', () => {
272
+ const tenant = tenantId.parse(ulid());
273
+ beforeAll(async () => {
274
+ await host.admin.createTenant(staff, { id: tenant, slug: 'vertical-grammar', name: 'Vertical grammar' });
275
+ });
276
+ it.each(['', ' ', '\t', 'Todo', 'my_vertical', 'a/b/c', '/todo', 'todo/', '-todo', 'todo-', 'to do'])('refuses %j as validation_failed and stores nothing', async (vertical) => {
277
+ const scope = scopeId.parse(ulid());
278
+ await expectRefusal(host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical }), 'validation_failed', /vertical/);
279
+ expect(await host.admin.getScopeRecord(staff, tenant, scope)).toBeUndefined();
280
+ // The directory still reads whole: nothing unparsable was written.
281
+ await expect(host.admin.listScopes(staff, { tenantId: tenant })).resolves.toBeInstanceOf(Array);
282
+ });
283
+ it.each([
284
+ ['a bare slug', 'todo', 'todo'],
285
+ ['digits', 'ticket0', 'ticket0'],
286
+ ['a leading digit', '2do', '2do'],
287
+ ['an owner prefix', 'acme/todo', 'acme/todo'],
288
+ ['null', null, null],
289
+ ['no vertical at all', undefined, null],
290
+ ])('provisions with %s and stores it as given', async (_name, vertical, stored) => {
291
+ const scope = scopeId.parse(ulid());
292
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: scope, ...(vertical === undefined ? {} : { vertical }) });
293
+ expect((await host.admin.getScopeRecord(staff, tenant, scope))?.vertical).toBe(stored);
294
+ });
295
+ it.each([
296
+ ['kind', ''],
297
+ ['kind', ' '],
298
+ ['kind', '\t'],
299
+ ['name', ''],
300
+ ['name', ' '],
301
+ ['name', '\n'],
302
+ ])('refuses %s %j as validation_failed naming the field, and stores nothing', async (field, value) => {
303
+ const scope = scopeId.parse(ulid());
304
+ await expectRefusal(host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: 'todo', [field]: value }), 'validation_failed', new RegExp(`^provisionScope: ${field} `));
305
+ expect(await host.admin.getScopeRecord(staff, tenant, scope)).toBeUndefined();
306
+ });
307
+ it.each([
308
+ ['given', { kind: 'brf', name: 'Brf Vasastan' }, { kind: 'brf', name: 'Brf Vasastan' }],
309
+ ['given with spaces around them, as sent', { kind: ' brf ', name: ' Brf ' }, { kind: ' brf ', name: ' Brf ' }],
310
+ ['absent, as the defaults', {}, { kind: 'scope' }],
311
+ ])('provisions with kind and name %s', async (_case, given, stored) => {
312
+ const scope = scopeId.parse(ulid());
313
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: 'todo', ...given });
314
+ const row = await host.admin.getScopeRecord(staff, tenant, scope);
315
+ expect(row).toMatchObject(stored);
316
+ // An absent name defaults to the slug, which defaults to the lowercased id.
317
+ if (!('name' in given))
318
+ expect(row?.name).toBe(scope.toLowerCase());
319
+ });
320
+ it('refuses an invalid vertical on a re-provision of an existing scope too', async () => {
321
+ const scope = scopeId.parse(ulid());
322
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: 'todo' });
323
+ await expectRefusal(host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: '' }), 'validation_failed');
324
+ expect((await host.admin.getScopeRecord(staff, tenant, scope))?.vertical).toBe('todo');
325
+ });
326
+ });
271
327
  it('fails closed on a mismatched (tenantId, scopeId) pair (K-3)', async () => {
272
328
  await expect(host.getScope(alice, t2, s1)).rejects.toThrow();
273
329
  await expect(host.getScope(alice, t1, scopeId.parse(ulid()))).rejects.toThrow();
@@ -2312,6 +2368,21 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2312
2368
  // The spine: present and flagged system, so the UI can group it apart.
2313
2369
  expect(byName.get('_substrat_outbox')?.system).toBe(true);
2314
2370
  });
2371
+ it('counts what is in force apart from what a table keeps as tombstones (#2531)', async () => {
2372
+ const key = `introspect-${ulid().toLowerCase()}`;
2373
+ await host.admin.defineRole(staff, t1, { key, permissions: [permissionKey.parse('thing:read')], source: 'vertical' });
2374
+ const seat = { principalId: principalId.parse(ulid()), roleKey: key, node: { tenantId: t1, scopeId: s1 } };
2375
+ const tuples = async () => (await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_tuples');
2376
+ const before = await tuples();
2377
+ expect(before.liveRowCount).toBeDefined();
2378
+ await host.admin.assignRole(staff, seat);
2379
+ expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount + 1 });
2380
+ // A revoke tombstones the seat: the row is kept, and it is no longer in force.
2381
+ await host.admin.unassignRole(staff, seat);
2382
+ expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount });
2383
+ // A table that keeps no tombstones answers no live count.
2384
+ expect((await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_outbox')).not.toHaveProperty('liveRowCount');
2385
+ });
2315
2386
  it('reads a bounded page of a table', async () => {
2316
2387
  const stub = await host.getScope(alice, t1, s1);
2317
2388
  await stub.invoke('test/write-marker', { v: 'page-row' });
@@ -3015,6 +3086,84 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3015
3086
  expect((await failureOf(operation)).message).toBe(REDACTED_FAILURE_NOTE);
3016
3087
  expect((await issueOf(operation)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3017
3088
  });
3089
+ it('#2540: a pair filed under another tenant, whose staff text names this subject, is redacted on both', async () => {
3090
+ // The pair is the other tenant's failure, and its withheld text names this tenant's subject.
3091
+ const erased = dataSubjectId.parse(ulid());
3092
+ const operation = op('pair');
3093
+ await host.admin.recordOpsFailure({
3094
+ actor: staff,
3095
+ operation,
3096
+ stage: 'terminal',
3097
+ tenantId: t2,
3098
+ message: 'the step failed (reference r)',
3099
+ detail: `a store refused while reading contact ${erased}`,
3100
+ reference: `erase${ulid().toLowerCase()}`,
3101
+ });
3102
+ // The issue's sample is the staff text, so it is the platform's.
3103
+ expect(await exemplarOwnerOf(operation)).toEqual({ kind: 'platform', tenant: null });
3104
+ expect((await issueOf(operation)).lastMessage).toContain(erased);
3105
+ await host.admin.shredSubject(staff, t1, s1, erased);
3106
+ expect((await issueOf(operation)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3107
+ const rows = await host.admin.listOpsFailures(staff, { operation });
3108
+ // Staff's row with no tenant goes on the id match; the other tenant's row never named it.
3109
+ expect(rows.map((r) => [r.tenantId, r.message])).toEqual([
3110
+ [t2, 'the step failed (reference r)'],
3111
+ [null, REDACTED_FAILURE_NOTE],
3112
+ ]);
3113
+ });
3114
+ it("#2554: the drain's pair about a tombstoned intent is redacted on staff's row and the platform's sample too", async () => {
3115
+ const kind = `connector:erasure-${ulid()}`;
3116
+ const erased = dataSubjectId.parse(ulid());
3117
+ const spared = dataSubjectId.parse(ulid());
3118
+ const linked = await routeIntent(kind, erased, 'Anna Ek');
3119
+ const unlinked = await routeIntent(kind, spared, 'Bo Lund');
3120
+ // As the drain files it (`withheldOpsFailure`): the tenant's row says what became of the
3121
+ // intent, staff's row (no tenant, the same scope) quotes the error, marker first.
3122
+ const pair = (operation, id, error) => host.admin.recordOpsFailure(withheldOpsFailure({ actor: staff, operation, stage: 'terminal', tenantId: t1, scopeId: s1 }, platformIntentFailureMessage(id, `failed: ${error}; reference = up${ulid().toLowerCase()}`), (r) => platformIntentFailureMessage(id, `failed (reference ${r})`)));
3123
+ const ops = { linked: op('pair-linked'), unlinked: op('pair-unlinked'), elsewhere: op('pair-elsewhere') };
3124
+ await pair(ops.linked, linked, 'HTTP 409: Anna Ek needs a personal number');
3125
+ await pair(ops.unlinked, unlinked, 'HTTP 409: Bo Lund needs a personal number');
3126
+ // The twin: a staff row naming the same intent under another scope is not this erasure's.
3127
+ await host.admin.recordOpsFailure({
3128
+ actor: staff, operation: ops.elsewhere, stage: 'terminal', tenantId: null, scopeId: s2,
3129
+ message: platformIntentFailureMessage(linked, 'failed: HTTP 409: Anna Ek'),
3130
+ });
3131
+ const rowsOf = async (operation) => (await host.admin.listOpsFailures(staff, { operation })).map((r) => [r.tenantId, r.message]);
3132
+ const before = { linked: await rowsOf(ops.linked), unlinked: await rowsOf(ops.unlinked), elsewhere: await rowsOf(ops.elsewhere) };
3133
+ expect(before.linked[1][1]).toMatch(/^\[upstream reference = up[0-9a-z]+\] platform intent /);
3134
+ expect(await exemplarOwnerOf(ops.linked)).toEqual({ kind: 'platform', tenant: null });
3135
+ await host.admin.shredSubject(staff, t1, s1, erased);
3136
+ // Both rows of the pair: the tenant's by the link it always had, staff's by #2554's.
3137
+ expect(await rowsOf(ops.linked)).toEqual([[t1, REDACTED_FAILURE_NOTE], [null, REDACTED_FAILURE_NOTE]]);
3138
+ expect((await issueOf(ops.linked)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3139
+ expect(await rowsOf(ops.unlinked)).toEqual(before.unlinked);
3140
+ expect((await issueOf(ops.unlinked)).lastMessage).toBe(before.unlinked[1][1]);
3141
+ expect(await rowsOf(ops.elsewhere)).toEqual(before.elsewhere);
3142
+ await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
3143
+ });
3144
+ it("#2556: staff's row alone, for a withheld pass that left a tombstoned intent pending, is redacted too", async () => {
3145
+ const kind = `connector:erasure-${ulid()}`;
3146
+ const erased = dataSubjectId.parse(ulid());
3147
+ const spared = dataSubjectId.parse(ulid());
3148
+ const linked = await routeIntent(kind, erased, 'Anna Ek');
3149
+ const unlinked = await routeIntent(kind, spared, 'Bo Lund');
3150
+ // As the drain files it (`withheldStaffFailure`): no tenant, the drained scope, marker first.
3151
+ const transient = (operation, id, error) => host.admin.recordOpsFailure({
3152
+ actor: staff,
3153
+ ...withheldStaffFailure({ operation, stage: 'transient', scopeId: s1 }, `${error}; reference = up${ulid().toLowerCase()}`, ulid(), platformIntentFailureMessage(id, 'pending (the call did not complete at its deployment): ')),
3154
+ });
3155
+ const ops = { linked: op('transient-linked'), unlinked: op('transient-unlinked') };
3156
+ await transient(ops.linked, linked, 'HTTP 500: Anna Ek is not on file');
3157
+ await transient(ops.unlinked, unlinked, 'HTTP 500: Bo Lund is not on file');
3158
+ const rowsOf = async (operation) => (await host.admin.listOpsFailures(staff, { operation })).map((r) => [r.tenantId, r.message]);
3159
+ const before = await rowsOf(ops.unlinked);
3160
+ expect((await rowsOf(ops.linked))[0][1]).toMatch(/^\[upstream reference = up[0-9a-z]+\] platform intent .* pending \(the call did not complete at its deployment\): HTTP 500: /);
3161
+ await host.admin.shredSubject(staff, t1, s1, erased);
3162
+ expect(await rowsOf(ops.linked)).toEqual([[null, REDACTED_FAILURE_NOTE]]);
3163
+ expect((await issueOf(ops.linked)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3164
+ expect(await rowsOf(ops.unlinked)).toEqual(before);
3165
+ await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
3166
+ });
3018
3167
  /** Whose an exemplar is, as the directory stores it — not on the issue read shape. */
3019
3168
  const exemplarOwnerOf = async (operation) => {
3020
3169
  const dump = await host.admin.exportDirectory(staff);
@@ -4340,6 +4489,85 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4340
4489
  expect(both.map((r) => r.code).sort()).toEqual(['permission_denied', null].sort());
4341
4490
  expect(both.map((r) => r.origin).sort()).toEqual(['platform', null].sort());
4342
4491
  });
4492
+ it('#2540: a tenant row with a detail is a pair under one reference, counted once', async () => {
4493
+ const reference = `pair${ulid().toLowerCase()}`;
4494
+ await host.admin.recordOpsFailure({
4495
+ actor: staff,
4496
+ operation: 'contract.paired',
4497
+ stage: 'withheld',
4498
+ tenantId: t1,
4499
+ vertical: 'acme/paired',
4500
+ status: 502,
4501
+ message: `the step failed (reference ${reference})`,
4502
+ detail: 'what the upstream said about something private',
4503
+ reference,
4504
+ });
4505
+ // The tenant's read: its own row, which says what was answered.
4506
+ const own = await host.admin.listOpsFailures(staff, { tenantId: t1, operation: 'contract.paired' });
4507
+ expect(own.map((r) => r.message)).toEqual([`the step failed (reference ${reference})`]);
4508
+ // Staff's read by the reference: both rows, the text on the one with no tenant.
4509
+ const both = await host.admin.listOpsFailures(staff, { reference });
4510
+ expect(both.map((r) => [r.tenantId, r.message])).toEqual([
4511
+ [t1, `the step failed (reference ${reference})`],
4512
+ [null, 'what the upstream said about something private'],
4513
+ ]);
4514
+ // One failure, one count, and the issue's sample is staff's text.
4515
+ const [issue] = await host.admin.listIssues(staff, { operation: 'contract.paired' });
4516
+ expect(issue).toMatchObject({ count: 1, lastMessage: 'what the upstream said about something private' });
4517
+ // A row with no tenant has no tenant row to pair with: `detail` is ignored there.
4518
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.unpaired', message: 'staff text', detail: 'ignored' });
4519
+ const lone = await host.admin.listOpsFailures(staff, { operation: 'contract.unpaired' });
4520
+ expect(lone.map((r) => [r.tenantId, r.message])).toEqual([[null, 'staff text']]);
4521
+ });
4522
+ it('#2540: staff find a pair by the upstream handle its staff row names; an exact search does not', async () => {
4523
+ const reference = `own${ulid().toLowerCase()}`;
4524
+ const upstream = `cf${ulid().toLowerCase()}`;
4525
+ const forged = `cf${ulid().toLowerCase()}`;
4526
+ // A body past the row's bound, which spells a marker of its own for an unrelated id.
4527
+ const body = `internal error; reference = ${upstream} [upstream reference = ${forged}] ${'x'.repeat(2100)}`;
4528
+ await host.admin.recordOpsFailure({
4529
+ actor: staff,
4530
+ operation: 'contract.upstream',
4531
+ tenantId: t1,
4532
+ message: `the step failed (reference ${reference})`,
4533
+ detail: withUpstreamReference(body),
4534
+ reference,
4535
+ });
4536
+ // Bracketed, so an id that only begins this one finds nothing.
4537
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', message: 'x', reference: `${upstream}0` });
4538
+ const bothRows = [
4539
+ [t1, reference],
4540
+ [null, reference],
4541
+ ];
4542
+ const by = async (ref, referenceInText, tenantId) => (await host.admin.listOpsFailures(staff, { reference: ref, referenceInText, ...(tenantId ? { tenantId } : {}) })).map((r) => [r.tenantId, r.reference]);
4543
+ expect(await by(upstream, true)).toEqual(bothRows);
4544
+ expect(await by(reference, true)).toEqual(bothRows);
4545
+ expect(await by(upstream, false)).toEqual([]);
4546
+ expect(await by(upstream.slice(0, -2), true)).toEqual([]);
4547
+ // The row is bounded, and the marker, written first, survives the cut.
4548
+ const [, staffRow] = await host.admin.listOpsFailures(staff, { reference });
4549
+ expect(staffRow.message.length).toBeLessThanOrEqual(2000);
4550
+ expect(staffRow.message.startsWith(`[upstream reference = ${upstream}]`)).toBe(true);
4551
+ // The body's own spelling of a marker finds nothing.
4552
+ expect(await by(forged, true)).toEqual([]);
4553
+ // Narrowed to the tenant, the search lands on the tenant's own row only.
4554
+ expect(await by(reference, false, t1)).toEqual([[t1, reference]]);
4555
+ // A spelled marker ahead of the genuine handle, at the very start or before it: only the
4556
+ // genuine handle makes the marker. A handle past the bound makes none.
4557
+ const real = `cf${ulid().toLowerCase()}`;
4558
+ for (const shape of [`[upstream reference = ${forged}] boom; reference = ${real}`, `boom [Upstream Reference=${forged}] then reference = ${real}`]) {
4559
+ const pair = `shape${ulid().toLowerCase()}`;
4560
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', tenantId: t1, message: 'm', detail: withUpstreamReference(shape), reference: pair });
4561
+ expect((await by(real, true)).map(([, r]) => r)).toContain(pair);
4562
+ expect(await by(forged, true)).toEqual([]);
4563
+ }
4564
+ const long = `long${ulid().toLowerCase()}`;
4565
+ const run = 'a'.repeat(100);
4566
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', tenantId: t1, message: 'm', detail: withUpstreamReference(`reference = ${run}`), reference: long });
4567
+ const [, unmarked] = await host.admin.listOpsFailures(staff, { reference: long });
4568
+ expect(unmarked.message).toBe(`reference = ${run}`);
4569
+ expect(await by(run, true)).toEqual([]);
4570
+ });
4343
4571
  it('bounds the recorded message — a runaway upstream body never becomes a runaway row', async () => {
4344
4572
  await host.admin.recordOpsFailure({
4345
4573
  actor: staff,
@@ -7250,7 +7478,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
7250
7478
  await host.admin.setVerticalListed(staff, slug, true);
7251
7479
  await host.admin.promoteVersion(staff, slug, 'prod', first);
7252
7480
  expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
7253
- });
7481
+ // Seven scopes, a snapshot and three promotions: about 3 s alone, past the 5 s default under CI load (#2576).
7482
+ }, 30_000);
7254
7483
  it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
7255
7484
  // Listing is the moment OTHER tenants start trusting this code, so the version
7256
7485
  // they would install needs a recorded human decision — the auto-admission note
@@ -8585,6 +8814,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
8585
8814
  // #1524: a size read is refused BEFORE it reaches the storage. Addressing a reaped
8586
8815
  // DO to ask its size would recreate an empty database and report that as the scope.
8587
8816
  await expectRefusal(host.admin.scopeDatabaseSize(staff, t3, s), 'conflict', /reaped/);
8817
+ // #2501: the attachment read is refused the same way, never answered as 0.
8818
+ if (host.admin.scopeAttachmentBytes) {
8819
+ await expectRefusal(host.admin.scopeAttachmentBytes(staff, t3, s), 'conflict', /reaped/);
8820
+ }
8588
8821
  // Audited as reapScope against the right scope + actor.
8589
8822
  const reapEntry = (await host.admin.auditLog(staff, { tenantId: t3 })).find((r) => r.action === 'reapScope' && r.scopeId === s);
8590
8823
  expect(reapEntry?.actor).toBe(staff);
@@ -9249,15 +9482,87 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9249
9482
  sampled: 1,
9250
9483
  });
9251
9484
  });
9252
- it("forgets a reaped scope's samples, and never writes one for it again", async () => {
9485
+ it('keeps attachment samples beside the databases, the day\'s latest, until retention, a reaped scope\'s too (#2501)', async () => {
9486
+ const ta = tenantId.parse(ulid());
9487
+ await host.admin.createTenant(staff, { id: ta, slug: 'gauge-attachments', name: 'Gauge Attachments' });
9488
+ const [s, quiet] = [await newScope(ta), await newScope(ta)];
9489
+ const attachmentsOf = async (tenant) => (await host.admin.readMeters(staff, { tenantId: tenant })).perTenant[0].storage;
9490
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '08:00:00'), attachments: 300 }]);
9491
+ // Beside the databases' figure, never inside it, which still names attachments as excluded.
9492
+ expect(await attachmentsOf(ta)).toMatchObject({ bytes: 100, excluded: ['attachments', 'tenant-stores', 'lake'], attachments: { bytes: 300, sampled: 1 } });
9493
+ // A later reading of the day replaces it; an older one, recorded after it, does not.
9494
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '09:00:00'), attachments: 50 }]);
9495
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '08:30:00'), attachments: 999 }]);
9496
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 1 });
9497
+ // A reading that did not measure attachments (unsupported, failed, declined) leaves them.
9498
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 120, readAt: day(0, '10:00:00') }]);
9499
+ expect(await attachmentsOf(ta)).toMatchObject({ bytes: 120, attachments: { bytes: 50, sampled: 1 } });
9500
+ // A scope with none holds 0 of them: a sample like any other, counted in `sampled`.
9501
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: quiet, bytes: 10, readAt: day(0, '10:00:00'), attachments: 0 }]);
9502
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9503
+ // Named under another tenant, refused like the database sample.
9504
+ const other = tenantId.parse(ulid());
9505
+ await host.admin.createTenant(staff, { id: other, slug: 'gauge-attachments-other', name: 'Gauge Attachments Other' });
9506
+ await newScope(other);
9507
+ await host.admin.recordScopeStorage(staff, [{ tenantId: other, scopeId: s, bytes: 1, readAt: day(0, '11:00:00'), attachments: 7 }]);
9508
+ expect((await attachmentsOf(other))?.attachments).toBeUndefined();
9509
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9510
+ // Past retention a sample goes with the database samples, the limit shared across the tables.
9511
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9512
+ ;
9513
+ const old = new Date();
9514
+ old.setUTCMonth(old.getUTCMonth() - 14);
9515
+ const aged = tenantId.parse(ulid());
9516
+ await host.admin.createTenant(staff, { id: aged, slug: 'gauge-attachments-old', name: 'Gauge Attachments Old' });
9517
+ const agedScope = await newScope(aged);
9518
+ await host.admin.recordScopeStorage(staff, [{ tenantId: aged, scopeId: agedScope, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9519
+ expect((await attachmentsOf(aged))?.attachments).toEqual({ bytes: 9, sampled: 1 });
9520
+ expect(await host.admin.pruneScopeStorage(staff, 1)).toBe(1);
9521
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(1);
9522
+ expect((await attachmentsOf(aged))?.attachments).toBeUndefined();
9523
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9524
+ // A reaped scope's attachment samples follow its database days (#2515): the reap keeps
9525
+ // them for the invoice and the gauge, which describes live storage, stops counting them.
9526
+ const reaped = await newScope(ta);
9527
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: reaped, bytes: 1, readAt: old.toISOString(), attachments: 4 }]);
9528
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: reaped, bytes: 1, readAt: day(0, '12:00:00'), attachments: 3 }]);
9529
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 53, sampled: 3 });
9530
+ await host.admin.archiveScope(staff, ta, reaped);
9531
+ await host.admin.reapScope(staff, ta, reaped);
9532
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9533
+ // Retention, not the reap, ends them: the old database day and the old attachment day are
9534
+ // both still there for the prune to take, and the current ones stay.
9535
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9536
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([1]);
9537
+ });
9538
+ it('a tenant reap forgets its attachment samples with its other storage rows (#2501)', async () => {
9539
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9540
+ ;
9541
+ const gone = tenantId.parse(ulid());
9542
+ await host.admin.createTenant(staff, { id: gone, slug: 'gauge-attachments-tenant-reap', name: 'Gauge Attachments Tenant Reap' });
9543
+ const s = await newScope(gone);
9544
+ // Past retention, so the prune (which reads the raw tables) finds the row if the reap left it.
9545
+ const old = new Date();
9546
+ old.setUTCMonth(old.getUTCMonth() - 14);
9547
+ await host.admin.recordScopeStorage(staff, [{ tenantId: gone, scopeId: s, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9548
+ // Its twin, before the reap: both old samples are there for the prune to find.
9549
+ const kept = tenantId.parse(ulid());
9550
+ await host.admin.createTenant(staff, { id: kept, slug: 'gauge-attachments-tenant-kept', name: 'Gauge Attachments Tenant Kept' });
9551
+ const ks = await newScope(kept);
9552
+ await host.admin.recordScopeStorage(staff, [{ tenantId: kept, scopeId: ks, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9553
+ await host.admin.setTenantStatus(staff, gone, 'deleting');
9554
+ await host.admin.reapTenant(staff, gone);
9555
+ // Only the kept tenant's two rows (database and attachments) were left to prune.
9556
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9557
+ });
9558
+ it("keeps a reaped scope's days for the invoice, ends its attempts, and never writes one for it again", async () => {
9253
9559
  const tr = tenantId.parse(ulid());
9254
9560
  await host.admin.createTenant(staff, { id: tr, slug: 'gauge-reap', name: 'Gauge Reap' });
9255
9561
  const [kept, reaped] = [await newScope(tr), await newScope(tr)];
9256
9562
  await record(tr, kept, 1, day(0, '02:00:00'));
9257
9563
  await record(tr, reaped, 2, day(-1, '02:00:00'));
9258
9564
  await record(tr, reaped, 3, day(0, '02:00:00'));
9259
- // A sample past retention, so the prune below (which reads the raw table, with no join
9260
- // to scope status) can tell whether reap deleted the rows or only the join hides them.
9565
+ // A sample past retention: retention, not the reap, is what ends a reaped scope's days.
9261
9566
  while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9262
9567
  ;
9263
9568
  const old = new Date();
@@ -9265,14 +9570,25 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9265
9570
  expect(await record(tr, reaped, 5, old.toISOString())).toEqual({ recorded: 1 });
9266
9571
  await host.admin.archiveScope(staff, tr, reaped);
9267
9572
  await host.admin.reapScope(staff, tr, reaped);
9268
- expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(0);
9269
- expect((await host.admin.listScopeStorage(staff, { tenantId: tr })).map((r) => r.scopeId)).toEqual([kept]);
9573
+ // Right after the reap all three days are there, the old one included: the reap deleted none.
9574
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([5, 2, 3]);
9575
+ // The month it was reaped in is still readable for the tenant, scope by scope.
9576
+ const history = await host.admin.listScopeStorage(staff, { tenantId: tr, since: day(-1, '00:00:00').slice(0, 10) });
9577
+ expect(history.filter((r) => r.scopeId === reaped).map((r) => r.bytes)).toEqual([2, 3]);
9578
+ expect(history.filter((r) => r.scopeId === kept).map((r) => r.bytes)).toEqual([1]);
9579
+ // The current gauge and the attempts describe live storage only.
9580
+ expect((await host.admin.listScopeStorage(staff, { tenantId: tr, latest: true })).map((r) => r.scopeId)).toEqual([kept]);
9581
+ expect((await host.admin.listScopeStorageAttempts(staff)).some((a) => a.scopeId === reaped)).toBe(false);
9270
9582
  expect(await record(tr, reaped, 4, day(0, '03:00:00'))).toEqual({ recorded: 0 });
9271
9583
  expect((await host.admin.readMeters(staff, { tenantId: tr })).perTenant[0].storage).toMatchObject({
9272
9584
  bytes: 1,
9273
9585
  sampled: 1,
9274
9586
  total: 1, // the tombstone is not in the denominator
9275
9587
  });
9588
+ // Retention, not the reap, takes the one past thirteen months, and only it.
9589
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9590
+ ;
9591
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([2, 3]);
9276
9592
  });
9277
9593
  it('keeps an archived scope in the figure and refreshes it, so the tenant is not stuck stale', async () => {
9278
9594
  const ta = tenantId.parse(ulid());
@@ -9375,6 +9691,23 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9375
9691
  await host.deleteSnapshot(staff, tk, fork);
9376
9692
  expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(0);
9377
9693
  });
9694
+ it("deletes a fork's attachment samples with its other samples, and keeps its source's (#2501)", async () => {
9695
+ const tk = tenantId.parse(ulid());
9696
+ await host.admin.createTenant(staff, { id: tk, slug: 'gauge-fork-attachments', name: 'Gauge Fork Attachments' });
9697
+ const source = await newScope(tk);
9698
+ const fork = await host.snapshotScope(staff, tk, source);
9699
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9700
+ ;
9701
+ const old = new Date();
9702
+ old.setUTCMonth(old.getUTCMonth() - 14);
9703
+ // Past retention, so the prune (which reads the raw tables) finds whatever the delete left.
9704
+ for (const s of [source, fork]) {
9705
+ await host.admin.recordScopeStorage(staff, [{ tenantId: tk, scopeId: s, bytes: 1, readAt: old.toISOString(), attachments: 5 }]);
9706
+ }
9707
+ await host.deleteSnapshot(staff, tk, fork);
9708
+ // Only the source's database and attachment samples were left to prune: the fork's went with it.
9709
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9710
+ });
9378
9711
  it('prunes samples older than thirteen months, at most the limit per call', async () => {
9379
9712
  const tp = tenantId.parse(ulid());
9380
9713
  await host.admin.createTenant(staff, { id: tp, slug: 'gauge-prune', name: 'Gauge Prune' });
@@ -9395,6 +9728,984 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9395
9728
  await expect(host.admin.pruneScopeStorage(staff, -1)).rejects.toThrow(/limit/);
9396
9729
  });
9397
9730
  });
9731
+ // -- the daily request rollup (#2501) ----------------------------------------
9732
+ // -- the capability record (RFC wire-signing §7, Q8) ---------------------------
9733
+ describe('the capability record (RFC wire-signing §7, Q8)', () => {
9734
+ // Each test owns its verticals: the records are fleet-wide.
9735
+ const slugOf = (name) => `${name}-${ulid().slice(-8).toLowerCase()}`;
9736
+ const record = (over = {}) => ({
9737
+ envelope: 1,
9738
+ entrySkipped: null,
9739
+ state: 'dual',
9740
+ audience: 'acme-serving',
9741
+ routerKids: ['If4x36FUomE', 'fX5Yi3zNBp0'],
9742
+ platformKids: ['OfcT0KZEJT8'],
9743
+ withheld: null,
9744
+ ...over,
9745
+ });
9746
+ /** A record of an upload that gave no lists, and why. */
9747
+ const withheld = (reason) => record({ state: 'legacy-only', audience: null, routerKids: [], platformKids: [], withheld: reason });
9748
+ const at = (time) => `2026-10-24T${time}.000Z`;
9749
+ /** The whole fleet, one full page: every read names its reach and its limit. */
9750
+ const fleet = { reach: { fleet: true }, limit: LIST_PAGE_MAX };
9751
+ /** The verticals this block registered: a record is written only beside its vertical's registration. */
9752
+ const registered = new Set();
9753
+ const register = async (slug, ownerTenant) => {
9754
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli', ...(ownerTenant ? { ownerTenant } : {}) });
9755
+ registered.add(slug);
9756
+ };
9757
+ const forget = async (slug) => {
9758
+ await host.admin.deleteVertical(staff, slug);
9759
+ registered.delete(slug);
9760
+ };
9761
+ /** Record `rec` for the script `<vertical>-<script>`, registering the vertical (platform-owned) first when no test did. */
9762
+ const put = async (vertical, script, versionId, rec, time) => {
9763
+ if (!registered.has(vertical))
9764
+ await register(vertical);
9765
+ await host.admin.recordWireAuth(staff, { deploymentRef: `${vertical}-${script}`, verticalSlug: vertical, versionId, record: rec, recordedAt: at(time) });
9766
+ };
9767
+ it('keeps one record per deployment, the last upload’s, and lists one vertical’s by deployment', async () => {
9768
+ const [a, b] = [slugOf('wire-a'), slugOf('wire-b')];
9769
+ await put(a, 'serving', 'v1', record({ audience: `${a}-serving` }), '10:00:00');
9770
+ await put(a, 'archive', 'v1', record({ audience: `${a}-archive` }), '10:00:01');
9771
+ await put(b, 'serving', null, withheld('not-in-rollout'), '10:00:02');
9772
+ // The serving script uploaded again, for another version, given nothing this time: its record is replaced.
9773
+ await put(a, 'serving', 'v2', withheld('lists-refused'), '10:05:00');
9774
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([
9775
+ { deploymentRef: `${a}-archive`, verticalSlug: a, versionId: 'v1', record: record({ audience: `${a}-archive` }), recordedAt: at('10:00:01') },
9776
+ { deploymentRef: `${a}-serving`, verticalSlug: a, versionId: 'v2', record: withheld('lists-refused'), recordedAt: at('10:05:00') },
9777
+ ]);
9778
+ const all = await host.admin.listWireAuth(staff, { ...fleet });
9779
+ expect(all.filter((r) => r.verticalSlug === a || r.verticalSlug === b).map((r) => r.deploymentRef)).toEqual([`${a}-archive`, `${a}-serving`, `${b}-serving`]);
9780
+ // An entry the uploader left off is recorded as such: no envelope, and why.
9781
+ await put(b, 'serving', null, record({ envelope: null, entrySkipped: 'an older bundle' }), '10:06:00');
9782
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: b }))[0].record).toMatchObject({ envelope: null, entrySkipped: 'an older bundle' });
9783
+ });
9784
+ it('writes each record to the admin log, naming the vertical, the script and the version, never a key', async () => {
9785
+ const a = slugOf('wire-log');
9786
+ await put(a, 'serving', 'v1', record(), '11:00:00');
9787
+ const rows = (await host.admin.auditLog(staff, { action: 'recordWireAuth' })).filter((r) => r.vertical === a);
9788
+ expect(rows).toHaveLength(1);
9789
+ expect(rows[0].actor).toBe(staff);
9790
+ expect(rows[0].after).toEqual({ vertical: a, deploymentRef: `${a}-serving`, versionId: 'v1', record: record() });
9791
+ });
9792
+ it('refuses a record that does not parse, or contradicts itself, and stores nothing for it', async () => {
9793
+ const a = slugOf('wire-bad');
9794
+ const good = { deploymentRef: `${a}-serving`, verticalSlug: a, versionId: null, record: record(), recordedAt: at('12:00:00') };
9795
+ for (const bad of [
9796
+ { ...good, deploymentRef: '' },
9797
+ { ...good, verticalSlug: '' },
9798
+ { ...good, recordedAt: 'yesterday' },
9799
+ { ...good, record: { ...record(), envelope: 2 } },
9800
+ { ...good, record: { ...record(), state: 'on' } },
9801
+ { ...good, record: { ...record(), withheld: 'because' } },
9802
+ // An envelope beside a reason the entry was left off, or neither.
9803
+ { ...good, record: record({ entrySkipped: 'an older bundle' }) },
9804
+ { ...good, record: record({ envelope: null }) },
9805
+ // Lists given beside a reason none were, lists given naming no key, and a reason with lists still named.
9806
+ { ...good, record: record({ withheld: 'unconfigured' }) },
9807
+ { ...good, record: record({ routerKids: [] }) },
9808
+ { ...good, record: record({ platformKids: [] }) },
9809
+ { ...good, record: { ...withheld('unconfigured'), routerKids: ['If4x36FUomE'] } },
9810
+ { ...good, record: { ...withheld('unconfigured'), state: 'dual' } },
9811
+ ]) {
9812
+ await expectRefusal(host.admin.recordWireAuth(staff, bad), 'validation_failed');
9813
+ }
9814
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
9815
+ });
9816
+ it('reads a page at a time by script name, and each read is access-logged', async () => {
9817
+ const a = slugOf('wire-page');
9818
+ for (const [i, script] of ['a1', 'a2', 'a3'].entries())
9819
+ await put(a, script, null, record(), `14:00:0${i}`);
9820
+ const reach = { fleet: true };
9821
+ const first = await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: 2 });
9822
+ expect(first.map((r) => r.deploymentRef)).toEqual([`${a}-a1`, `${a}-a2`]);
9823
+ const rest = await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: 2, cursor: first.at(-1).deploymentRef });
9824
+ expect(rest.map((r) => r.deploymentRef)).toEqual([`${a}-a3`]);
9825
+ expect((await host.admin.listWireAuth(staff, { reach, verticalSlug: a, order: 'desc', limit: 1 })).map((r) => r.deploymentRef)).toEqual([`${a}-a3`]);
9826
+ // The cap is a full page, and one past it is refused, not clamped.
9827
+ expect(await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: LIST_PAGE_MAX })).toHaveLength(3);
9828
+ const reads = (await host.admin.accessLog(staff, { method: 'listWireAuth' }))
9829
+ .map((e) => ({ actor: e.actor, params: JSON.parse(e.params ?? 'null'), count: e.resultCount }))
9830
+ .filter((e) => e.params?.['verticalSlug'] === a);
9831
+ expect(reads).toEqual([
9832
+ { actor: staff, params: { reach, verticalSlug: a, limit: 2 }, count: 2 },
9833
+ { actor: staff, params: { reach, verticalSlug: a, limit: 2, cursor: `${a}-a2` }, count: 1 },
9834
+ { actor: staff, params: { reach, verticalSlug: a, order: 'desc', limit: 1 }, count: 1 },
9835
+ { actor: staff, params: { reach, verticalSlug: a, limit: LIST_PAGE_MAX }, count: 3 },
9836
+ ]);
9837
+ });
9838
+ it('refuses a read that does not name its reach and its limit, and logs no access for it', async () => {
9839
+ const a = slugOf('wire-refuse');
9840
+ await put(a, 'serving', null, record(), '15:00:00');
9841
+ const t = ulid();
9842
+ for (const bad of [
9843
+ // No filter, no reach, an empty reach, both reaches, a reach that is not one.
9844
+ undefined,
9845
+ { limit: 10, verticalSlug: a },
9846
+ { reach: {}, limit: 10, verticalSlug: a },
9847
+ { reach: { tenantId: t, fleet: true }, limit: 10, verticalSlug: a },
9848
+ { reach: { fleet: false }, limit: 10, verticalSlug: a },
9849
+ { reach: { fleet: 'yes' }, limit: 10, verticalSlug: a },
9850
+ { reach: { tenantId: 'acme' }, limit: 10, verticalSlug: a },
9851
+ { reach: { tenantId: '' }, limit: 10, verticalSlug: a },
9852
+ { reach: { tenantId: t, extra: 1 }, limit: 10, verticalSlug: a },
9853
+ { reach: 'fleet', limit: 10, verticalSlug: a },
9854
+ // No limit, or one outside 1..LIST_PAGE_MAX.
9855
+ { reach: { fleet: true }, verticalSlug: a },
9856
+ ...[0, -1, 1.5, LIST_PAGE_MAX + 1, Number.POSITIVE_INFINITY, Number.NaN, '10'].map((limit) => ({ reach: { fleet: true }, limit, verticalSlug: a })),
9857
+ // The optional fields in their own types, and the text ones no longer than their bound.
9858
+ { reach: { fleet: true }, limit: 10, verticalSlug: '' },
9859
+ { reach: { fleet: true }, limit: 10, verticalSlug: 'x'.repeat(257) },
9860
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: 7 },
9861
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: '' },
9862
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: 'x'.repeat(257) },
9863
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, order: 'sideways' },
9864
+ // Only the filter's own keys: an unknown one is refused, never carried across or logged.
9865
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, tenantId: t },
9866
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, extra: 'x'.repeat(400) },
9867
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, offset: 0 },
9868
+ // Arrays are not a filter, nor a reach.
9869
+ [{ reach: { fleet: true }, limit: 10, verticalSlug: a }],
9870
+ { reach: [{ fleet: true }], limit: 10, verticalSlug: a },
9871
+ ]) {
9872
+ await expectRefusal(host.admin.listWireAuth(staff, bad), 'validation_failed');
9873
+ }
9874
+ const reads = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(a));
9875
+ expect(reads).toEqual([]);
9876
+ });
9877
+ it('confines a tenant’s read to the verticals it owns now', async () => {
9878
+ const [owner, other] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
9879
+ await host.admin.createTenant(staff, { id: owner, slug: `wire-own-${owner.slice(-6).toLowerCase()}`, name: 'Wire Owner' });
9880
+ await host.admin.createTenant(staff, { id: other, slug: `wire-oth-${other.slice(-6).toLowerCase()}`, name: 'Wire Other' });
9881
+ const [mine, theirs, platform] = [slugOf('wire-mine'), slugOf('wire-theirs'), slugOf('wire-plat')];
9882
+ await register(mine, owner);
9883
+ await register(theirs, other);
9884
+ await register(platform);
9885
+ for (const [i, slug] of [mine, theirs, platform].entries()) {
9886
+ await put(slug, 'serving', null, record(), `16:00:0${i}`);
9887
+ await put(slug, 'archive', 'v1', record(), `16:00:1${i}`);
9888
+ }
9889
+ const refs = async (reach, verticalSlug) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, ...(verticalSlug ? { verticalSlug } : {}) }))
9890
+ .map((r) => r.deploymentRef)
9891
+ .filter((ref) => [mine, theirs, platform].some((slug) => ref.startsWith(`${slug}-`)));
9892
+ expect(await refs({ tenantId: owner })).toEqual([`${mine}-archive`, `${mine}-serving`]);
9893
+ expect(await refs({ tenantId: other })).toEqual([`${theirs}-archive`, `${theirs}-serving`]);
9894
+ // Naming another tenant's vertical inside one tenant's reach reaches nothing, not that vertical.
9895
+ expect(await refs({ tenantId: owner }, theirs)).toEqual([]);
9896
+ expect(await refs({ tenantId: owner }, mine)).toEqual([`${mine}-archive`, `${mine}-serving`]);
9897
+ // A tenant that owns nothing reaches nothing; a vertical no tenant owns is reached by the fleet alone.
9898
+ expect(await refs({ tenantId: tenantId.parse(ulid()) })).toEqual([]);
9899
+ for (const slug of [mine, theirs, platform]) {
9900
+ expect(await refs({ fleet: true }, slug)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9901
+ }
9902
+ // A page inside the reach pages over the tenant's records only.
9903
+ const page = await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1 });
9904
+ expect(page.map((r) => r.deploymentRef)).toEqual([`${mine}-archive`]);
9905
+ const next = await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1, cursor: page[0].deploymentRef });
9906
+ expect(next.map((r) => r.deploymentRef)).toEqual([`${mine}-serving`]);
9907
+ expect(await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1, cursor: next[0].deploymentRef })).toEqual([]);
9908
+ });
9909
+ it('reads only the filter’s own fields, and echoes an unknown key no longer than its bound', async () => {
9910
+ const a = slugOf('wire-own');
9911
+ await put(a, 'archive', null, record(), '19:00:00');
9912
+ await put(a, 'serving', null, record(), '19:00:01');
9913
+ const refs = (rows) => rows.map((r) => r.deploymentRef);
9914
+ // An inherited cursor or order is not the caller's: it is neither applied nor judged, and the
9915
+ // judged copy that is logged carries none of it.
9916
+ const inherited = Object.assign(Object.create({ cursor: `${a}-serving`, order: 'sideways' }), {
9917
+ reach: { fleet: true },
9918
+ limit: LIST_PAGE_MAX,
9919
+ verticalSlug: a,
9920
+ });
9921
+ expect(refs(await host.admin.listWireAuth(staff, inherited))).toEqual([`${a}-archive`, `${a}-serving`]);
9922
+ const logged = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(a));
9923
+ expect(logged.map((e) => e.params)).toEqual([JSON.stringify({ reach: { fleet: true }, verticalSlug: a, limit: LIST_PAGE_MAX })]);
9924
+ // A reach or a limit only inherited is no reach or limit at all, and an inherited tenant is no tenant.
9925
+ for (const bad of [
9926
+ Object.create({ reach: { fleet: true }, limit: 10 }),
9927
+ Object.assign(Object.create({ limit: 10 }), { reach: { fleet: true }, verticalSlug: a }),
9928
+ Object.assign(Object.create({ reach: { fleet: true } }), { limit: 10, verticalSlug: a }),
9929
+ { reach: Object.create({ tenantId: ulid() }), limit: 10, verticalSlug: a },
9930
+ { reach: Object.create({ fleet: true }), limit: 10, verticalSlug: a },
9931
+ ]) {
9932
+ await expectRefusal(host.admin.listWireAuth(staff, bad), 'validation_failed');
9933
+ }
9934
+ // An unknown key is named in the refusal, cut to its bound with an ellipsis.
9935
+ const longKey = 'k'.repeat(400);
9936
+ const error = await host.admin.listWireAuth(staff, { reach: { fleet: true }, limit: 10, [longKey]: 1 }).then(() => null, (e) => e);
9937
+ expect(errorCodeOf(error)).toBe('validation_failed');
9938
+ const message = error.message;
9939
+ expect(message).toContain(`${'k'.repeat(64)}…`);
9940
+ expect(message).not.toContain('k'.repeat(65));
9941
+ });
9942
+ it('logs a tenant’s read against that tenant and the fleet’s against none, the judged filter with its reach first', async () => {
9943
+ const owner = tenantId.parse(ulid());
9944
+ await host.admin.createTenant(staff, { id: owner, slug: `wire-log-${owner.slice(-6).toLowerCase()}`, name: 'Wire Log' });
9945
+ const mine = slugOf('wire-logged');
9946
+ await register(mine, owner);
9947
+ await put(mine, 'serving', null, record(), '17:00:00');
9948
+ // The reach given last, as a caller may spell it: the log still names it first.
9949
+ await host.admin.listWireAuth(staff, { limit: 5, verticalSlug: mine, reach: { tenantId: owner } });
9950
+ const forTenant = await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner });
9951
+ expect(forTenant.map((e) => ({ tenantId: e.tenantId, params: e.params, count: e.resultCount }))).toEqual([
9952
+ { tenantId: owner, params: JSON.stringify({ reach: { tenantId: owner }, verticalSlug: mine, limit: 5 }), count: 1 },
9953
+ ]);
9954
+ // The fleet's read is logged against no tenant, and is told apart by its reach.
9955
+ await host.admin.listWireAuth(staff, { reach: { fleet: true }, limit: 5, verticalSlug: mine });
9956
+ const fleetRows = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(mine) && e.tenantId === null);
9957
+ expect(fleetRows.map((e) => e.params)).toEqual([JSON.stringify({ reach: { fleet: true }, verticalSlug: mine, limit: 5 })]);
9958
+ expect(await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner })).toHaveLength(1);
9959
+ // A vertical and a cursor at their longest cannot push the reach out of the logged text, which
9960
+ // the log keeps cut to a bounded length.
9961
+ const long = 'z'.repeat(WIRE_AUTH_FILTER_TEXT_MAX);
9962
+ await host.admin.listWireAuth(staff, { cursor: long, verticalSlug: long, limit: 5, reach: { tenantId: owner } });
9963
+ const logged = (await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner })).map((e) => e.params ?? '');
9964
+ expect(logged).toHaveLength(2);
9965
+ for (const params of logged)
9966
+ expect(params.startsWith(`{"reach":{"tenantId":"${owner}"}`)).toBe(true);
9967
+ });
9968
+ it('follows the vertical’s current owner, resolved when the read runs', async () => {
9969
+ const [first, second] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
9970
+ await host.admin.createTenant(staff, { id: first, slug: `wire-fst-${first.slice(-6).toLowerCase()}`, name: 'Wire First' });
9971
+ await host.admin.createTenant(staff, { id: second, slug: `wire-snd-${second.slice(-6).toLowerCase()}`, name: 'Wire Second' });
9972
+ const slug = slugOf('wire-owned');
9973
+ const refsOf = async (owner) => (await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => r.deploymentRef);
9974
+ // The owner reads what was recorded while it held the slug.
9975
+ await register(slug, first);
9976
+ await put(slug, 'archive', 'v0', record(), '18:00:00');
9977
+ expect(await refsOf(first)).toEqual([`${slug}-archive`]);
9978
+ await put(slug, 'serving', 'v1', record(), '18:00:01');
9979
+ expect(await refsOf(first)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9980
+ // An owner changes by deleting the vertical, which takes its records, and registering it again:
9981
+ // the tenant that held it reads nothing after, the new owner reads only what was recorded since.
9982
+ await forget(slug);
9983
+ await register(slug, second);
9984
+ expect(await refsOf(first)).toEqual([]);
9985
+ expect(await refsOf(second)).toEqual([]);
9986
+ await put(slug, 'serving', 'v2', record(), '18:00:02');
9987
+ expect(await refsOf(first)).toEqual([]);
9988
+ expect(await refsOf(second)).toEqual([`${slug}-serving`]);
9989
+ });
9990
+ it('writes no record, and no admin row, for a vertical the registry does not hold', async () => {
9991
+ const never = slugOf('wire-unreg');
9992
+ const gone = slugOf('wire-gone');
9993
+ await put(gone, 'serving', 'v1', record(), '20:00:00');
9994
+ await forget(gone);
9995
+ for (const slug of [never, gone]) {
9996
+ await expectRefusal(host.admin.recordWireAuth(staff, { deploymentRef: `${slug}-serving`, verticalSlug: slug, versionId: 'v2', record: record(), recordedAt: at('20:00:01') }), 'not_found');
9997
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: slug })).toEqual([]);
9998
+ }
9999
+ const rows = (await host.admin.auditLog(staff, { action: 'recordWireAuth' })).filter((r) => r.vertical === never || r.vertical === gone);
10000
+ // Only the one write made while `gone` was registered.
10001
+ expect(rows.map((r) => r.after.versionId)).toEqual(['v1']);
10002
+ // Registered again, the slug's records start empty: nothing written while unregistered survives.
10003
+ await register(gone);
10004
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: gone })).toEqual([]);
10005
+ });
10006
+ it('keeps a walk’s hold on a record, by binding name, and refuses a hold it cannot read', async () => {
10007
+ const a = slugOf('wire-hold');
10008
+ const hold = { reason: 'verify', bindings: ['SCOPE', 'SUBSTRAT_WIRE_STATE'], at: at('22:00:00') };
10009
+ await put(a, 'archive', 'v1', { ...record(), walkHold: hold }, '22:00:00');
10010
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(hold);
10011
+ // The next upload of the script writes a record without it.
10012
+ await put(a, 'archive', 'v1', record(), '22:00:01');
10013
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
10014
+ const good = { deploymentRef: `${a}-archive`, verticalSlug: a, versionId: 'v1', recordedAt: at('22:00:02') };
10015
+ for (const walkHold of [
10016
+ { ...hold, reason: 'because' },
10017
+ { ...hold, at: 'yesterday' },
10018
+ { ...hold, bindings: [''] },
10019
+ { ...hold, bindings: Array.from({ length: 65 }, (_, i) => `B${i}`) },
10020
+ { ...hold, value: 'not a field' },
10021
+ // The count of names past the cap is a positive whole number when given.
10022
+ { ...hold, more: 0 },
10023
+ { ...hold, more: 1.5 },
10024
+ ]) {
10025
+ await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), walkHold } }), 'validation_failed');
10026
+ }
10027
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
10028
+ // A hold of more names than it keeps: the first ones, and how many more.
10029
+ const many = { ...hold, bindings: Array.from({ length: 64 }, (_, i) => `B${i}`), more: 6 };
10030
+ await put(a, 'archive', 'v1', { ...record(), walkHold: many }, '22:00:02');
10031
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(many);
10032
+ await put(a, 'archive', 'v1', record(), '22:00:02');
10033
+ // `unverified`, the walk's record of a script it could not confirm: lists claimed by none, and
10034
+ // only beside a hold. Without the hold, or beside lists, it is refused.
10035
+ const unverified = withheld('unverified');
10036
+ await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: unverified }), 'validation_failed');
10037
+ await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), withheld: 'unverified', walkHold: hold } }), 'validation_failed');
10038
+ await put(a, 'archive', 'v1', { ...unverified, walkHold: hold }, '22:00:03');
10039
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record).toMatchObject({ withheld: 'unverified', state: 'legacy-only', routerKids: [], walkHold: hold });
10040
+ });
10041
+ it('audits a pass of the fleet walk as walkWireAuth, naming scripts and reasons, never a value', async () => {
10042
+ const entry = {
10043
+ apply: true,
10044
+ confirm: false,
10045
+ from: null,
10046
+ next: 'acme/crm|01HZZZZZZZZZZZZZZZZZZZZZZZ',
10047
+ walked: 3,
10048
+ updated: ['acme-crm'],
10049
+ held: ['acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz'],
10050
+ released: [],
10051
+ skipped: [{ deploymentRef: 'acme-crm-01hyyyyyyyyyyyyyyyyyyyyyyy', reason: 'no-entry' }],
10052
+ failed: [{ deploymentRef: 'acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz', reason: 'verify' }],
10053
+ };
10054
+ await host.admin.recordWireWalk(staff, entry);
10055
+ const rows = (await host.admin.auditLog(staff, { action: 'walkWireAuth' })).filter((r) => r.after.next === entry.next);
10056
+ expect(rows).toHaveLength(1);
10057
+ expect(rows[0].actor).toBe(staff);
10058
+ expect(rows[0].after).toEqual(entry);
10059
+ });
10060
+ it('restores the owners and the records together, from one dump', async () => {
10061
+ const [first, second] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
10062
+ await host.admin.createTenant(staff, { id: first, slug: `wire-rst-${first.slice(-6).toLowerCase()}`, name: 'Wire Restore' });
10063
+ const slug = slugOf('wire-restore');
10064
+ await register(slug, first);
10065
+ await put(slug, 'serving', 'v1', record(), '21:00:00');
10066
+ const backup = await host.admin.exportDirectory(staff);
10067
+ // Diverge past the copy: the vertical deleted (its records with it) and taken by another tenant.
10068
+ await host.admin.createTenant(staff, { id: second, slug: `wire-rs2-${second.slice(-6).toLowerCase()}`, name: 'Wire Restore 2' });
10069
+ await forget(slug);
10070
+ await register(slug, second);
10071
+ await put(slug, 'archive', 'v2', record(), '21:00:01');
10072
+ await host.admin.restoreDirectory(staff, backup);
10073
+ registered.add(slug);
10074
+ // The copy's owner reads the copy's records, and nothing of the divergence came back with it.
10075
+ const reads = async (reach) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => [r.deploymentRef, r.versionId]);
10076
+ expect(await reads({ tenantId: first })).toEqual([[`${slug}-serving`, 'v1']]);
10077
+ expect(await reads({ tenantId: second })).toEqual([]);
10078
+ expect(await reads({ fleet: true })).toEqual([[`${slug}-serving`, 'v1']]);
10079
+ });
10080
+ it('forgets a vertical’s records with the vertical', async () => {
10081
+ const a = slugOf('wire-del');
10082
+ await register(a);
10083
+ await put(a, 'serving', null, record(), '13:00:00');
10084
+ const kept = slugOf('wire-kept');
10085
+ await put(kept, 'serving', null, record(), '13:00:01');
10086
+ await forget(a);
10087
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
10088
+ // Another vertical's record is not touched.
10089
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: kept })).toHaveLength(1);
10090
+ });
10091
+ });
10092
+ describe('the daily request rollup (#2501)', () => {
10093
+ // Each test owns its days, because the rollup's record of a day is fleet-wide.
10094
+ const dayAgo = (n) => new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10);
10095
+ const monthsAgo = (m) => {
10096
+ const d = new Date();
10097
+ d.setUTCMonth(d.getUTCMonth() - m);
10098
+ return d.toISOString().slice(0, 10);
10099
+ };
10100
+ const at = (day, time) => `${day}T${time}.000Z`;
10101
+ const ids = () => ({ tenant: tenantId.parse(ulid()), scope: scopeId.parse(ulid()) });
10102
+ // `n` requests, all served (2xx) unless a split is given.
10103
+ const served = (n, split = {}) => ({ requests: n, class2xx: n, class3xx: 0, class4xx: 0, class5xx: 0, rateLimited: 0, ...split });
10104
+ it('keeps one row per (day, tenant, vertical, scope) with its status split, every count an estimate, and a later read replaces the day', async () => {
10105
+ const day = dayAgo(2);
10106
+ const [a, b] = [ids(), ids()];
10107
+ // 40 routed: 30 served, 1 redirected, 7 refused (5 of them the router's rate limit), 2 failed.
10108
+ const split = { class2xx: 30, class3xx: 1, class4xx: 7, class5xx: 2, rateLimited: 5 };
10109
+ expect(await host.admin.recordRequestDay(staff, {
10110
+ day,
10111
+ attemptedAt: at(day, '23:00:00'),
10112
+ counts: [
10113
+ { tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(40, split), sampled: true },
10114
+ // A request that resolved to no vertical is kept under none.
10115
+ { tenantId: a.tenant, vertical: null, scopeId: a.scope, ...served(2), sampled: false },
10116
+ { tenantId: b.tenant, vertical: 'acme-crm', scopeId: b.scope, ...served(5), sampled: false },
10117
+ ],
10118
+ })).toEqual({ recorded: 3 });
10119
+ expect(await host.admin.listRequestDays(staff, { tenantId: a.tenant })).toEqual([
10120
+ { day, tenantId: a.tenant, vertical: null, scopeId: a.scope, ...served(2), sampled: false, estimated: true },
10121
+ { day, tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(40, split), sampled: true, estimated: true },
10122
+ ]);
10123
+ // Read again, the day is what the newer read counted: a key it no longer names is gone.
10124
+ await host.admin.recordRequestDay(staff, {
10125
+ day,
10126
+ attemptedAt: at(day, '23:30:00'),
10127
+ counts: [{ tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(41), sampled: true }],
10128
+ });
10129
+ expect((await host.admin.listRequestDays(staff, { since: day, until: day })).map((r) => [r.tenantId, r.requests])).toEqual([
10130
+ [a.tenant, 41],
10131
+ ]);
10132
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10133
+ {
10134
+ day,
10135
+ attemptedAt: at(day, '23:30:00'),
10136
+ readAt: at(day, '23:30:00'),
10137
+ rows: 1,
10138
+ error: null,
10139
+ saturated: false,
10140
+ failingSince: null,
10141
+ failingCount: null,
10142
+ },
10143
+ ]);
10144
+ // K-24: both reads are attributable.
10145
+ const reads = await host.admin.accessLog(staff, { method: 'listRequestDays' });
10146
+ expect(reads.some((r) => r.tenantId === a.tenant)).toBe(true);
10147
+ expect((await host.admin.accessLog(staff, { method: 'listRequestRollupDays' })).length).toBeGreaterThan(0);
10148
+ });
10149
+ it('records a failed read as the attempt, writes nothing of the day, and keeps a read that succeeded', async () => {
10150
+ const day = dayAgo(3);
10151
+ const t = ids();
10152
+ expect(await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '01:00:00'), error: 'HTTP 503' })).toEqual({
10153
+ recorded: 0,
10154
+ });
10155
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10156
+ // A failure named no run, so it starts one of its own.
10157
+ {
10158
+ day,
10159
+ attemptedAt: at(day, '01:00:00'),
10160
+ readAt: null,
10161
+ rows: null,
10162
+ error: 'HTTP 503',
10163
+ saturated: false,
10164
+ failingSince: at(day, '01:00:00'),
10165
+ failingCount: 1,
10166
+ },
10167
+ ]);
10168
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([]);
10169
+ // A success clears the error, and ends the run the failure had started.
10170
+ await host.admin.recordRequestDay(staff, {
10171
+ day,
10172
+ attemptedAt: at(day, '02:00:00'),
10173
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(9), sampled: false }],
10174
+ });
10175
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10176
+ readAt: at(day, '02:00:00'),
10177
+ error: null,
10178
+ failingSince: null,
10179
+ failingCount: null,
10180
+ });
10181
+ // A later failure records the attempt and leaves the day's rows and its successful read. It
10182
+ // names the run it extends, and the record keeps that run's start and length.
10183
+ await host.admin.recordRequestDay(staff, {
10184
+ day,
10185
+ attemptedAt: at(day, '03:00:00'),
10186
+ error: 'HTTP 503',
10187
+ failingSince: at(day, '00:30:00'),
10188
+ failingCount: 4,
10189
+ });
10190
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10191
+ {
10192
+ day,
10193
+ attemptedAt: at(day, '03:00:00'),
10194
+ readAt: at(day, '02:00:00'),
10195
+ rows: 1,
10196
+ error: 'HTTP 503',
10197
+ saturated: false,
10198
+ failingSince: at(day, '00:30:00'),
10199
+ failingCount: 4,
10200
+ },
10201
+ ]);
10202
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), error: 'HTTP 503', failingCount: 0 })).rejects.toThrow(/failingCount/);
10203
+ expect((await host.admin.listRequestDays(staff, { since: day, until: day })).map((r) => r.requests)).toEqual([9]);
10204
+ });
10205
+ it('records a saturated day as a standing state on its record, and a later read clears it', async () => {
10206
+ const day = dayAgo(8);
10207
+ const t = ids();
10208
+ await host.admin.recordRequestDay(staff, {
10209
+ day,
10210
+ attemptedAt: at(day, '01:00:00'),
10211
+ error: 'more than 50000 groups',
10212
+ saturated: true,
10213
+ failingSince: at(day, '00:00:00'),
10214
+ failingCount: 3,
10215
+ });
10216
+ // Saturated belongs to no run of failures, even when it is handed one.
10217
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10218
+ {
10219
+ day,
10220
+ attemptedAt: at(day, '01:00:00'),
10221
+ readAt: null,
10222
+ rows: null,
10223
+ error: 'more than 50000 groups',
10224
+ saturated: true,
10225
+ failingSince: null,
10226
+ failingCount: null,
10227
+ },
10228
+ ]);
10229
+ // Its twin: an ordinary failure of the same day is not saturated, and does start a run.
10230
+ await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '02:00:00'), error: 'HTTP 503' });
10231
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10232
+ saturated: false,
10233
+ failingCount: 1,
10234
+ });
10235
+ await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '03:00:00'), error: 'too many', saturated: true });
10236
+ await host.admin.recordRequestDay(staff, {
10237
+ day,
10238
+ attemptedAt: at(day, '04:00:00'),
10239
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(2), sampled: false }],
10240
+ });
10241
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10242
+ readAt: at(day, '04:00:00'),
10243
+ error: null,
10244
+ saturated: false,
10245
+ failingSince: null,
10246
+ failingCount: null,
10247
+ });
10248
+ });
10249
+ it('refuses a malformed count whole, writing none of the day; the same day well formed is written', async () => {
10250
+ const day = dayAgo(4);
10251
+ const t = ids();
10252
+ const good = { tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(3), sampled: false };
10253
+ await expect(host.admin.recordRequestDay(staff, {
10254
+ day,
10255
+ attemptedAt: at(day, '04:00:00'),
10256
+ counts: [good, { ...good, tenantId: 'not-a-tenant-id', ...served(1) }],
10257
+ })).rejects.toThrow();
10258
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), counts: [good, { ...good, requests: -1 }] })).rejects.toThrow();
10259
+ // A split that does not fit inside its total, or 429s beyond the 4xx, is malformed too.
10260
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), counts: [good, { ...good, ...served(3, { class5xx: 1 }) }] })).rejects.toThrow();
10261
+ await expect(host.admin.recordRequestDay(staff, {
10262
+ day,
10263
+ attemptedAt: at(day, '04:00:00'),
10264
+ counts: [good, { ...good, ...served(3, { class2xx: 2, class4xx: 1, rateLimited: 2 }) }],
10265
+ })).rejects.toThrow();
10266
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([]);
10267
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([]);
10268
+ // Two counts for one key are summed, every column of them, never one dropped. Each column
10269
+ // is non-zero on both sides and differs between them, so a column taken from one side
10270
+ // instead of summed cannot pass for the sum.
10271
+ const one = served(4, { class2xx: 1, class3xx: 1, class4xx: 1, class5xx: 1, rateLimited: 1 });
10272
+ const other = served(10, { class2xx: 2, class3xx: 3, class4xx: 2, class5xx: 3, rateLimited: 2 });
10273
+ expect(await host.admin.recordRequestDay(staff, {
10274
+ day,
10275
+ attemptedAt: at(day, '05:00:00'),
10276
+ counts: [
10277
+ { ...good, ...one },
10278
+ { ...good, ...other, sampled: true },
10279
+ ],
10280
+ })).toEqual({ recorded: 1 });
10281
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([
10282
+ {
10283
+ day,
10284
+ tenantId: t.tenant,
10285
+ vertical: 'acme-crm',
10286
+ scopeId: t.scope,
10287
+ ...served(14, { class2xx: 3, class3xx: 4, class4xx: 3, class5xx: 4, rateLimited: 3 }),
10288
+ sampled: true,
10289
+ estimated: true,
10290
+ },
10291
+ ]);
10292
+ });
10293
+ it('filters by tenant, vertical, scope and an inclusive window of days, and bounds the page', async () => {
10294
+ const [d5, d6, d7] = [dayAgo(5), dayAgo(6), dayAgo(7)];
10295
+ const t = tenantId.parse(ulid());
10296
+ const [s1, s2] = [scopeId.parse(ulid()), scopeId.parse(ulid())];
10297
+ for (const day of [d5, d6, d7]) {
10298
+ await host.admin.recordRequestDay(staff, {
10299
+ day,
10300
+ attemptedAt: at(day, '23:59:00'),
10301
+ counts: [
10302
+ { tenantId: t, vertical: 'acme-crm', scopeId: s1, ...served(1), sampled: false },
10303
+ { tenantId: t, vertical: 'acme-desk', scopeId: s2, ...served(2), sampled: false },
10304
+ ],
10305
+ });
10306
+ }
10307
+ expect(await host.admin.listRequestDays(staff, { tenantId: t })).toHaveLength(6);
10308
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, since: d6 })).map((r) => r.day)).toEqual([d6, d6, d5, d5]);
10309
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, until: d6 })).map((r) => r.day)).toEqual([d7, d7, d6, d6]);
10310
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, vertical: 'acme-desk' })).map((r) => r.scopeId)).toEqual([s2, s2, s2]);
10311
+ expect(await host.admin.listRequestDays(staff, { tenantId: t, scopeId: s1, since: d5 })).toHaveLength(1);
10312
+ expect(await host.admin.listRequestDays(staff, { tenantId: t, limit: 2 })).toHaveLength(2);
10313
+ await expect(host.admin.listRequestDays(staff, { limit: 0 })).rejects.toThrow(/limit/);
10314
+ await expect(host.admin.listRequestDays(staff, { since: '2026-13-01' })).rejects.toThrow(/UTC day/);
10315
+ });
10316
+ it('prunes days older than an explicit horizon, at most the limit per call; a null horizon prunes nothing', async () => {
10317
+ const t = ids();
10318
+ const days = [monthsAgo(16), monthsAgo(15), monthsAgo(14), monthsAgo(2), dayAgo(1)];
10319
+ for (const day of days) {
10320
+ await host.admin.recordRequestDay(staff, {
10321
+ day,
10322
+ attemptedAt: at(day, '12:00:00'),
10323
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(1), sampled: false }],
10324
+ });
10325
+ }
10326
+ // The default keeps everything: nothing goes, however old.
10327
+ expect(await host.admin.pruneRequestDays(staff, 500, null)).toBe(0);
10328
+ expect(await host.admin.listRequestDays(staff, { tenantId: t.tenant })).toHaveLength(5);
10329
+ // Its positive twin: thirteen months named, the three older days go, two per call.
10330
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(2);
10331
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(1);
10332
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(0);
10333
+ expect((await host.admin.listRequestDays(staff, { tenantId: t.tenant })).map((r) => r.day)).toEqual([monthsAgo(2), dayAgo(1)]);
10334
+ // The record of a pruned day goes with it; a kept day's stays.
10335
+ const recorded = (await host.admin.listRequestRollupDays(staff)).map((r) => r.day);
10336
+ expect(recorded).not.toContain(monthsAgo(14));
10337
+ expect(recorded).toEqual(expect.arrayContaining([monthsAgo(2), dayAgo(1)]));
10338
+ await expect(host.admin.pruneRequestDays(staff, 0, 13)).rejects.toThrow(/limit/);
10339
+ await expect(host.admin.pruneRequestDays(staff, 10, 0)).rejects.toThrow(/whole number of months/);
10340
+ });
10341
+ it('the sweep phase rolls up due days into this directory, stops at a failed day, and retries it only after an hour', async () => {
10342
+ // Days far from every other test's, so the fleet-wide record of a day is this test's alone.
10343
+ const now = '2031-05-10T12:00:00.000Z';
10344
+ const t = ids();
10345
+ const asked = [];
10346
+ let failing = '2031-05-08';
10347
+ const options = (nowIso) => ({
10348
+ actor: staff,
10349
+ fetch: connectorTestFetch,
10350
+ sweepers: {},
10351
+ drainRetries: false,
10352
+ requestRollup: {
10353
+ lookbackDays: 3,
10354
+ now: () => nowIso,
10355
+ read: async (day) => {
10356
+ asked.push(day);
10357
+ if (day === failing)
10358
+ throw new Error('Cloudflare Analytics Engine query failed: HTTP 503');
10359
+ return [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(Number(day.slice(-2))), sampled: false }];
10360
+ },
10361
+ },
10362
+ });
10363
+ // The failed day ends the pass's reads: the day after it is left for the next pass.
10364
+ const first = await runPlatformSweep(host, options(now));
10365
+ expect(asked).toEqual(['2031-05-07', '2031-05-08']);
10366
+ expect(first.requestRollup).toMatchObject({ due: 3, read: 1, failed: 1, recorded: 1 });
10367
+ expect(first.errors.filter((e) => e.kind === 'request-rollup')).toEqual([
10368
+ { kind: 'request-rollup', id: '2031-05-08', error: expect.stringMatching(/HTTP 503/) },
10369
+ ]);
10370
+ expect((await host.admin.listRequestDays(staff, { tenantId: t.tenant })).map((r) => [r.day, r.requests])).toEqual([
10371
+ ['2031-05-07', 7],
10372
+ ]);
10373
+ // Inside the hour the failed day is not read again; the day after it is.
10374
+ failing = '';
10375
+ const soon = await runPlatformSweep(host, options('2031-05-10T12:30:00.000Z'));
10376
+ expect(soon.requestRollup).toMatchObject({ due: 1, read: 1 });
10377
+ expect(asked.slice(2)).toEqual(['2031-05-09']);
10378
+ // An hour on, the failed day is read, and only it.
10379
+ const later = await runPlatformSweep(host, options('2031-05-10T13:00:00.000Z'));
10380
+ expect(later.requestRollup).toMatchObject({ due: 1, read: 1, failed: 0, recorded: 1 });
10381
+ expect(asked.slice(3)).toEqual(['2031-05-08']);
10382
+ expect((await host.admin.listRequestRollupDays(staff, { since: '2031-05-07', until: '2031-05-09' })).map((r) => [r.day, r.error])).toEqual([
10383
+ ['2031-05-07', null],
10384
+ ['2031-05-08', null],
10385
+ ['2031-05-09', null],
10386
+ ]);
10387
+ });
10388
+ });
10389
+ // -- the daily meter snapshot (#2501) -----------------------------------------
10390
+ describe('the daily meter snapshot (#2501)', () => {
10391
+ // A snapshot reads the whole directory, which every other test also writes to, so each test
10392
+ // narrows its reads to its own tenants. The record of a day is fleet-wide, so each test owns its
10393
+ // days: far from today, and from each other's.
10394
+ const newTenant = async () => {
10395
+ const t = tenantId.parse(ulid());
10396
+ await host.admin.createTenant(staff, { id: t, slug: `meter-day-${t.toLowerCase()}`, name: 'Meter day' });
10397
+ return t;
10398
+ };
10399
+ const newScope = async (tenant, vertical) => {
10400
+ const s = scopeId.parse(ulid());
10401
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: s, ...(vertical ? { vertical } : {}) });
10402
+ await host.admin.activateScope(staff, tenant, s);
10403
+ return s;
10404
+ };
10405
+ const counts = (c) => ({
10406
+ total: 0,
10407
+ active: 0,
10408
+ suspended: 0,
10409
+ provisioning: 0,
10410
+ archived: 0,
10411
+ reaped: 0,
10412
+ ...c,
10413
+ });
10414
+ const monthsAgo = (m) => {
10415
+ const d = new Date();
10416
+ d.setUTCMonth(d.getUTCMonth() - m);
10417
+ return d.toISOString().slice(0, 10);
10418
+ };
10419
+ it('keeps one row per tenant not reaped: its scopes by effective status, split by vertical, and each grant live or lapsed at the instant', async () => {
10420
+ const at = '2032-03-04T00:07:00.000Z';
10421
+ const t = await newTenant();
10422
+ await newScope(t, 'meter-crm');
10423
+ await newScope(t, 'meter-crm');
10424
+ const archived = await newScope(t, 'meter-crm');
10425
+ await newScope(t, 'meter-desk');
10426
+ await newScope(t);
10427
+ await host.admin.archiveScope(staff, t, archived);
10428
+ await host.admin.grantEntitlement(staff, t, 'meter-sku', { plan: 'pro' });
10429
+ // Lapsed AT the instant: a grant is live only while its expiry is after the reading.
10430
+ await host.admin.grantEntitlement(staff, t, 'meter-aux', { expiresAt: at });
10431
+ // Its twin, a millisecond later, is live.
10432
+ await host.admin.grantEntitlement(staff, t, 'meter-trial', { expiresAt: '2032-03-04T00:07:00.001Z' });
10433
+ // A suspended tenant's active scope is counted suspended, and the tenant bills nothing.
10434
+ const off = await newTenant();
10435
+ await newScope(off, 'meter-crm');
10436
+ await host.admin.setTenantStatus(staff, off, 'suspended');
10437
+ // A reaped tenant is a tombstone: no row. Its twin, a tenant being deleted, has one.
10438
+ const [gone, going] = [await newTenant(), await newTenant()];
10439
+ await host.admin.setTenantStatus(staff, gone, 'deleting');
10440
+ await host.admin.reapTenant(staff, gone);
10441
+ await host.admin.setTenantStatus(staff, going, 'deleting');
10442
+ const taken = await host.admin.recordMeterDay(staff, { at });
10443
+ expect(taken).toMatchObject({ day: '2032-03-04', taken: true });
10444
+ const fleet = await host.admin.listMeterDays(staff, { since: '2032-03-04', until: '2032-03-04', limit: 10_000 });
10445
+ expect(taken.tenants).toBe(fleet.length);
10446
+ expect(fleet.map((r) => r.tenantId)).not.toContain(gone);
10447
+ expect(fleet.find((r) => r.tenantId === going)).toMatchObject({ status: 'deleting', billable: false, scopes: counts({}) });
10448
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([
10449
+ {
10450
+ day: '2032-03-04',
10451
+ tenantId: t,
10452
+ readAt: at,
10453
+ status: 'active',
10454
+ billable: true,
10455
+ scopes: counts({ total: 5, active: 4, archived: 1 }),
10456
+ // Null first, then by vertical; the split sums to the tenant's counts.
10457
+ verticals: [
10458
+ { vertical: null, ...counts({ total: 1, active: 1 }) },
10459
+ { vertical: 'meter-crm', ...counts({ total: 3, active: 2, archived: 1 }) },
10460
+ { vertical: 'meter-desk', ...counts({ total: 1, active: 1 }) },
10461
+ ],
10462
+ entitlements: [
10463
+ { entitlementKey: 'meter-aux', plan: null, expiresAt: at, live: false },
10464
+ { entitlementKey: 'meter-sku', plan: 'pro', expiresAt: null, live: true },
10465
+ { entitlementKey: 'meter-trial', plan: null, expiresAt: '2032-03-04T00:07:00.001Z', live: true },
10466
+ ],
10467
+ },
10468
+ ]);
10469
+ expect((await host.admin.listMeterDays(staff, { tenantId: off }))[0]).toMatchObject({
10470
+ status: 'suspended',
10471
+ billable: false,
10472
+ scopes: counts({ total: 1, suspended: 1 }),
10473
+ verticals: [{ vertical: 'meter-crm', ...counts({ total: 1, suspended: 1 }) }],
10474
+ });
10475
+ expect(await host.admin.listMeterDayTakes(staff, { since: '2032-03-04', until: '2032-03-04' })).toEqual([
10476
+ {
10477
+ day: '2032-03-04',
10478
+ attemptedAt: at,
10479
+ takenAt: at,
10480
+ tenants: fleet.length,
10481
+ error: null,
10482
+ failingSince: null,
10483
+ failingCount: null,
10484
+ },
10485
+ ]);
10486
+ // K-24: both reads are attributable, the first to the tenant it narrowed to.
10487
+ expect((await host.admin.accessLog(staff, { method: 'listMeterDays' })).some((r) => r.tenantId === t)).toBe(true);
10488
+ expect((await host.admin.accessLog(staff, { method: 'listMeterDayTakes' })).length).toBeGreaterThan(0);
10489
+ });
10490
+ it('agrees with /meters read at the same moment', async () => {
10491
+ const t = await newTenant();
10492
+ await newScope(t, 'meter-crm');
10493
+ const archived = await newScope(t);
10494
+ await host.admin.archiveScope(staff, t, archived);
10495
+ await host.admin.grantEntitlement(staff, t, 'meter-sku', { plan: 'pro' });
10496
+ await host.admin.grantEntitlement(staff, t, 'meter-old', { expiresAt: new Date(Date.now() - 86_400_000).toISOString() });
10497
+ const live = (await host.admin.readMeters(staff, { tenantId: t })).perTenant[0];
10498
+ await host.admin.recordMeterDay(staff, { at: new Date().toISOString() });
10499
+ const [kept] = await host.admin.listMeterDays(staff, { tenantId: t });
10500
+ expect(kept.scopes).toEqual(live.scopes);
10501
+ expect(kept.billable).toBe(live.billable);
10502
+ expect({
10503
+ live: kept.entitlements.filter((g) => g.live).length,
10504
+ expired: kept.entitlements.filter((g) => !g.live).length,
10505
+ }).toEqual(live.entitlements);
10506
+ });
10507
+ it('the first snapshot of a day stands: a later attempt that day writes nothing, and a failure never overwrites it', async () => {
10508
+ const t = await newTenant();
10509
+ await newScope(t);
10510
+ const first = '2032-04-01T00:05:00.000Z';
10511
+ expect(await host.admin.recordMeterDay(staff, { at: first })).toMatchObject({ taken: true });
10512
+ await newScope(t);
10513
+ expect(await host.admin.recordMeterDay(staff, { at: '2032-04-01T00:20:00.000Z' })).toEqual({
10514
+ day: '2032-04-01',
10515
+ taken: false,
10516
+ tenants: 0,
10517
+ });
10518
+ // A failure recorded after the snapshot committed (its caller stopped waiting) is not the day's record.
10519
+ await host.admin.recordMeterDay(staff, { at: '2032-04-01T00:35:00.000Z', error: 'took longer than 30000 ms' });
10520
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([
10521
+ expect.objectContaining({ readAt: first, scopes: counts({ total: 1, active: 1 }) }),
10522
+ ]);
10523
+ expect((await host.admin.listMeterDayTakes(staff, { since: '2032-04-01', until: '2032-04-01' }))[0]).toMatchObject({
10524
+ attemptedAt: first,
10525
+ takenAt: first,
10526
+ error: null,
10527
+ });
10528
+ // Its positive twin: the next day is a new snapshot, and sees the second scope.
10529
+ expect(await host.admin.recordMeterDay(staff, { at: '2032-04-02T00:05:00.000Z' })).toMatchObject({ taken: true });
10530
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => [r.day, r.scopes.total])).toEqual([
10531
+ ['2032-04-01', 1],
10532
+ ['2032-04-02', 2],
10533
+ ]);
10534
+ });
10535
+ it('records a failed attempt with its run and writes nothing of the day; a snapshot later that day clears the run', async () => {
10536
+ const t = await newTenant();
10537
+ const day = '2032-05-01';
10538
+ expect(await host.admin.recordMeterDay(staff, { at: `${day}T00:05:00.000Z`, error: 'directory busy' })).toEqual({
10539
+ day,
10540
+ taken: false,
10541
+ tenants: 0,
10542
+ });
10543
+ // A failure that names no run starts one of its own.
10544
+ expect(await host.admin.listMeterDayTakes(staff, { since: day, until: day })).toEqual([
10545
+ {
10546
+ day,
10547
+ attemptedAt: `${day}T00:05:00.000Z`,
10548
+ takenAt: null,
10549
+ tenants: null,
10550
+ error: 'directory busy',
10551
+ failingSince: `${day}T00:05:00.000Z`,
10552
+ failingCount: 1,
10553
+ },
10554
+ ]);
10555
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([]);
10556
+ // A failure that extends a run keeps its start and length.
10557
+ await host.admin.recordMeterDay(staff, {
10558
+ at: `${day}T00:20:00.000Z`,
10559
+ error: 'directory busy',
10560
+ failingSince: '2032-04-30T23:50:00.000Z',
10561
+ failingCount: 4,
10562
+ });
10563
+ expect((await host.admin.listMeterDayTakes(staff, { since: day, until: day }))[0]).toMatchObject({
10564
+ attemptedAt: `${day}T00:20:00.000Z`,
10565
+ failingSince: '2032-04-30T23:50:00.000Z',
10566
+ failingCount: 4,
10567
+ });
10568
+ await expect(host.admin.recordMeterDay(staff, { at: `${day}T00:25:00.000Z`, error: 'directory busy', failingCount: 0 })).rejects.toThrow(/failingCount/);
10569
+ // A snapshot taken later the same day stands, and ends the run.
10570
+ await host.admin.recordMeterDay(staff, { at: `${day}T00:35:00.000Z` });
10571
+ expect((await host.admin.listMeterDayTakes(staff, { since: day, until: day }))[0]).toEqual({
10572
+ day,
10573
+ attemptedAt: `${day}T00:35:00.000Z`,
10574
+ takenAt: `${day}T00:35:00.000Z`,
10575
+ tenants: expect.any(Number),
10576
+ error: null,
10577
+ failingSince: null,
10578
+ failingCount: null,
10579
+ });
10580
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toHaveLength(1);
10581
+ });
10582
+ it('filters by tenant and an inclusive window of days, and bounds the page', async () => {
10583
+ const t = await newTenant();
10584
+ for (const day of ['2032-06-01', '2032-06-02', '2032-06-03']) {
10585
+ await host.admin.recordMeterDay(staff, { at: `${day}T01:00:00.000Z` });
10586
+ }
10587
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => r.day)).toEqual(['2032-06-01', '2032-06-02', '2032-06-03']);
10588
+ expect((await host.admin.listMeterDays(staff, { tenantId: t, since: '2032-06-02' })).map((r) => r.day)).toEqual(['2032-06-02', '2032-06-03']);
10589
+ expect((await host.admin.listMeterDays(staff, { tenantId: t, until: '2032-06-02' })).map((r) => r.day)).toEqual(['2032-06-01', '2032-06-02']);
10590
+ expect(await host.admin.listMeterDays(staff, { tenantId: t, limit: 2 })).toHaveLength(2);
10591
+ expect((await host.admin.listMeterDayTakes(staff, { since: '2032-06-02', until: '2032-06-03' })).map((r) => r.day)).toEqual([
10592
+ '2032-06-02',
10593
+ '2032-06-03',
10594
+ ]);
10595
+ await expect(host.admin.listMeterDays(staff, { limit: 0 })).rejects.toThrow(/limit/);
10596
+ await expect(host.admin.listMeterDays(staff, { since: '2032-13-01' })).rejects.toThrow(/UTC day/);
10597
+ await expect(host.admin.listMeterDayTakes(staff, { until: '2032-02-30' })).rejects.toThrow(/UTC day/);
10598
+ });
10599
+ it('prunes snapshots older than an explicit horizon, at most the limit per call; a null horizon prunes nothing', async () => {
10600
+ // Two tenants, so every day holds at least two rows and a prune of one leaves the day in part.
10601
+ const t = await newTenant();
10602
+ await newTenant();
10603
+ const old = [monthsAgo(16), monthsAgo(15), monthsAgo(14)];
10604
+ const kept = [monthsAgo(2)];
10605
+ for (const day of [...old, ...kept])
10606
+ await host.admin.recordMeterDay(staff, { at: `${day}T12:00:00.000Z` });
10607
+ // The default keeps everything: nothing goes, however old.
10608
+ expect(await host.admin.pruneMeterDays(staff, 500, null)).toBe(0);
10609
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toHaveLength(4);
10610
+ // Its positive twin: thirteen months named, the older days go, at most the limit per call.
10611
+ expect(await host.admin.pruneMeterDays(staff, 1, 13)).toBe(1);
10612
+ // The oldest day still holds a row, so it is still marked taken and cannot be taken again.
10613
+ expect(await host.admin.listMeterDayTakes(staff, { since: old[0], until: old[0] })).toHaveLength(1);
10614
+ while ((await host.admin.pruneMeterDays(staff, 500, 13)) > 0)
10615
+ ;
10616
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => r.day)).toEqual(kept);
10617
+ // The record of a pruned day goes with it; a kept day's stays.
10618
+ const recorded = (await host.admin.listMeterDayTakes(staff, { since: old[0], until: kept[0] })).map((r) => r.day);
10619
+ expect(recorded).toEqual(kept);
10620
+ await expect(host.admin.pruneMeterDays(staff, 0, 13)).rejects.toThrow(/limit/);
10621
+ await expect(host.admin.pruneMeterDays(staff, 10, 0)).rejects.toThrow(/whole number of months/);
10622
+ });
10623
+ it('the sweep phase takes the day on its first pass into this directory, and not again until the next day', async () => {
10624
+ const t = await newTenant();
10625
+ await newScope(t, 'meter-crm');
10626
+ const pass = (now) => runPlatformSweep(host, {
10627
+ actor: staff,
10628
+ fetch: connectorTestFetch,
10629
+ sweepers: {},
10630
+ drainRetries: false,
10631
+ meterSnapshot: { now: () => now },
10632
+ });
10633
+ const first = await pass('2033-01-01T00:04:00.000Z');
10634
+ expect(first.errors.filter((e) => e.kind === 'meter-snapshot')).toEqual([]);
10635
+ expect(first.meterSnapshot).toMatchObject({ day: '2033-01-01', taken: true, failed: 0, streak: null });
10636
+ const later = await pass('2033-01-01T00:19:00.000Z');
10637
+ expect(later.meterSnapshot).toMatchObject({ day: '2033-01-01', taken: false, tenants: 0, failed: 0 });
10638
+ const next = await pass('2033-01-02T00:04:00.000Z');
10639
+ expect(next.meterSnapshot).toMatchObject({ day: '2033-01-02', taken: true });
10640
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => [r.day, r.readAt, r.scopes.active])).toEqual([
10641
+ ['2033-01-01', '2033-01-01T00:04:00.000Z', 1],
10642
+ ['2033-01-02', '2033-01-02T00:04:00.000Z', 1],
10643
+ ]);
10644
+ });
10645
+ });
10646
+ // -- the history of vertical ownership (#2501) ---------------------------------
10647
+ describe('the history of vertical ownership (#2501)', () => {
10648
+ const newSlug = () => `owned-${ulid().toLowerCase()}`;
10649
+ const newOwner = async () => {
10650
+ const t = tenantId.parse(ulid());
10651
+ await host.admin.createTenant(staff, { id: t, slug: `owner-${t.toLowerCase()}`, name: 'Owner' });
10652
+ return t;
10653
+ };
10654
+ const createdAtOf = async (slug) => (await host.admin.listVerticals(staff)).find((v) => v.slug === slug).createdAt;
10655
+ it('opens a period with the registration and closes it with the delete; a slug registered again opens a new one', async () => {
10656
+ const [first, second] = [await newOwner(), await newOwner()];
10657
+ const slug = newSlug();
10658
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
10659
+ const since = await createdAtOf(slug);
10660
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner: first, since, until: null }]);
10661
+ // An identical registration opens nothing; a conflicting one is refused and opens nothing.
10662
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
10663
+ await expect(host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: second })).rejects.toThrow();
10664
+ expect(await host.admin.listVerticalOwners(staff, slug)).toHaveLength(1);
10665
+ // The delete closes the period, and the history outlives the registry row.
10666
+ await host.admin.deleteVertical(staff, slug);
10667
+ const [closed] = await host.admin.listVerticalOwners(staff, slug);
10668
+ expect(closed).toEqual({ slug, owner: first, since, until: expect.any(String) });
10669
+ expect(closed.until >= since).toBe(true);
10670
+ // Registered again, by another tenant: the first owner's period stays closed, a new one opens.
10671
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: second });
10672
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([
10673
+ closed,
10674
+ { slug, owner: second, since: await createdAtOf(slug), until: null },
10675
+ ]);
10676
+ // K-24: the read is attributable.
10677
+ expect((await host.admin.accessLog(staff, { method: 'listVerticalOwners' })).length).toBeGreaterThan(0);
10678
+ await host.admin.deleteVertical(staff, slug);
10679
+ });
10680
+ it("a platform vertical's owner is none, and a slug never registered has no period", async () => {
10681
+ const slug = newSlug();
10682
+ await host.admin.registerVertical(staff, { slug, name: 'Platform', source: 'builtin' });
10683
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner: null, since: await createdAtOf(slug), until: null }]);
10684
+ expect(await host.admin.listVerticalOwners(staff, newSlug())).toEqual([]);
10685
+ await host.admin.deleteVertical(staff, slug);
10686
+ });
10687
+ it("a tenant reap keeps the periods it owned: the history is the invoice's, kept for good", async () => {
10688
+ const owner = await newOwner();
10689
+ const [past, standing] = [newSlug(), newSlug()];
10690
+ await host.admin.registerVertical(staff, { slug: past, name: 'Past', source: 'cli', ownerTenant: owner });
10691
+ await host.admin.deleteVertical(staff, past);
10692
+ await host.admin.registerVertical(staff, { slug: standing, name: 'Standing', source: 'cli', ownerTenant: owner });
10693
+ const before = [await host.admin.listVerticalOwners(staff, past), await host.admin.listVerticalOwners(staff, standing)];
10694
+ await host.admin.setTenantStatus(staff, owner, 'deleting');
10695
+ await host.admin.reapTenant(staff, owner);
10696
+ expect([await host.admin.listVerticalOwners(staff, past), await host.admin.listVerticalOwners(staff, standing)]).toEqual(before);
10697
+ expect(before.flat().map((p) => p.owner)).toEqual([owner, owner]);
10698
+ await host.admin.deleteVertical(staff, standing);
10699
+ });
10700
+ it('a delete refused while a scope still runs the vertical leaves the period open', async () => {
10701
+ const owner = await newOwner();
10702
+ const slug = newSlug();
10703
+ await host.admin.registerVertical(staff, { slug, name: 'Bound', source: 'cli', ownerTenant: owner });
10704
+ await host.provisionScope(staff, { tenantId: owner, scopeId: scopeId.parse(ulid()), vertical: slug });
10705
+ await expect(host.admin.deleteVertical(staff, slug)).rejects.toThrow(/still backs/);
10706
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner, since: await createdAtOf(slug), until: null }]);
10707
+ });
10708
+ });
9398
10709
  it('leaves bare (module-less) operations ungated', async () => {
9399
10710
  // test/read-counter was registered via defineOperation, no manifest — it
9400
10711
  // must resolve regardless of entitlements.
@@ -9468,6 +10779,16 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9468
10779
  name: 'Impostor Co',
9469
10780
  }), 'conflict', /already taken/);
9470
10781
  });
10782
+ it('words the tenant slug refusal without the tenant that holds the slug (#2556)', async () => {
10783
+ const holder = tenantId.parse(ulid());
10784
+ const slug = `held-${holder.toLowerCase()}`;
10785
+ await host.admin.createTenant(staff, { id: holder, slug, name: 'Holder Co' });
10786
+ const taken = await host.admin
10787
+ .createTenant(staff, { id: tenantId.parse(ulid()), slug, name: 'Impostor Co' })
10788
+ .then(() => null, (e) => e);
10789
+ expect(taken?.message).toBe(`tenant slug '${slug}' is already taken (slugs are unique)`);
10790
+ expect(taken?.message).not.toContain(holder);
10791
+ });
9471
10792
  it('enumerates the scopes under a tenant, and filters by status (§4.5)', async () => {
9472
10793
  const all = await host.admin.listScopes(staff, { tenantId: t5 });
9473
10794
  expect(all.length).toBeGreaterThanOrEqual(2);