@substrat-run/contract-tests 0.159.0 → 0.160.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 (67) 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 +17 -0
  8. package/dist/directory-audit-suite.d.ts.map +1 -0
  9. package/dist/directory-audit-suite.js +345 -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 +8 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +10 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/many-migrations-suite.d.ts +9 -0
  19. package/dist/many-migrations-suite.d.ts.map +1 -0
  20. package/dist/many-migrations-suite.js +76 -0
  21. package/dist/many-migrations-suite.js.map +1 -0
  22. package/dist/many-migrations.d.ts +16 -0
  23. package/dist/many-migrations.d.ts.map +1 -0
  24. package/dist/many-migrations.js +50 -0
  25. package/dist/many-migrations.js.map +1 -0
  26. package/dist/membership-executor-suite.d.ts.map +1 -1
  27. package/dist/membership-executor-suite.js +8 -5
  28. package/dist/membership-executor-suite.js.map +1 -1
  29. package/dist/modules.d.ts.map +1 -1
  30. package/dist/modules.js +10 -0
  31. package/dist/modules.js.map +1 -1
  32. package/dist/reserved-role-suite.d.ts +6 -0
  33. package/dist/reserved-role-suite.d.ts.map +1 -1
  34. package/dist/reserved-role-suite.js +176 -8
  35. package/dist/reserved-role-suite.js.map +1 -1
  36. package/dist/role-state-suite.d.ts +17 -0
  37. package/dist/role-state-suite.d.ts.map +1 -0
  38. package/dist/role-state-suite.js +80 -0
  39. package/dist/role-state-suite.js.map +1 -0
  40. package/dist/schedule-platform-requests-suite.d.ts +7 -1
  41. package/dist/schedule-platform-requests-suite.d.ts.map +1 -1
  42. package/dist/schedule-platform-requests-suite.js +349 -2
  43. package/dist/schedule-platform-requests-suite.js.map +1 -1
  44. package/dist/schedule-purge-request-module.d.ts +88 -0
  45. package/dist/schedule-purge-request-module.d.ts.map +1 -0
  46. package/dist/schedule-purge-request-module.js +114 -0
  47. package/dist/schedule-purge-request-module.js.map +1 -0
  48. package/dist/schedule-purge-two-module.d.ts +141 -0
  49. package/dist/schedule-purge-two-module.d.ts.map +1 -0
  50. package/dist/schedule-purge-two-module.js +153 -0
  51. package/dist/schedule-purge-two-module.js.map +1 -0
  52. package/dist/scope-host-suite.d.ts.map +1 -1
  53. package/dist/scope-host-suite.js +1177 -7
  54. package/dist/scope-host-suite.js.map +1 -1
  55. package/dist/size-reads-suite.d.ts +20 -0
  56. package/dist/size-reads-suite.d.ts.map +1 -0
  57. package/dist/size-reads-suite.js +77 -0
  58. package/dist/size-reads-suite.js.map +1 -0
  59. package/dist/stored-vertical-copy-suite.d.ts +15 -0
  60. package/dist/stored-vertical-copy-suite.d.ts.map +1 -0
  61. package/dist/stored-vertical-copy-suite.js +68 -0
  62. package/dist/stored-vertical-copy-suite.js.map +1 -0
  63. package/dist/unreadable-scope-row-suite.d.ts +16 -0
  64. package/dist/unreadable-scope-row-suite.d.ts.map +1 -0
  65. package/dist/unreadable-scope-row-suite.js +152 -0
  66. package/dist/unreadable-scope-row-suite.js.map +1 -0
  67. 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, 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();
@@ -3015,6 +3071,61 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3015
3071
  expect((await failureOf(operation)).message).toBe(REDACTED_FAILURE_NOTE);
3016
3072
  expect((await issueOf(operation)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3017
3073
  });
3074
+ it('#2540: a pair filed under another tenant, whose staff text names this subject, is redacted on both', async () => {
3075
+ // The pair is the other tenant's failure, and its withheld text names this tenant's subject.
3076
+ const erased = dataSubjectId.parse(ulid());
3077
+ const operation = op('pair');
3078
+ await host.admin.recordOpsFailure({
3079
+ actor: staff,
3080
+ operation,
3081
+ stage: 'terminal',
3082
+ tenantId: t2,
3083
+ message: 'the step failed (reference r)',
3084
+ detail: `a store refused while reading contact ${erased}`,
3085
+ reference: `erase${ulid().toLowerCase()}`,
3086
+ });
3087
+ // The issue's sample is the staff text, so it is the platform's.
3088
+ expect(await exemplarOwnerOf(operation)).toEqual({ kind: 'platform', tenant: null });
3089
+ expect((await issueOf(operation)).lastMessage).toContain(erased);
3090
+ await host.admin.shredSubject(staff, t1, s1, erased);
3091
+ expect((await issueOf(operation)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3092
+ const rows = await host.admin.listOpsFailures(staff, { operation });
3093
+ // Staff's row with no tenant goes on the id match; the other tenant's row never named it.
3094
+ expect(rows.map((r) => [r.tenantId, r.message])).toEqual([
3095
+ [t2, 'the step failed (reference r)'],
3096
+ [null, REDACTED_FAILURE_NOTE],
3097
+ ]);
3098
+ });
3099
+ it("#2554: the drain's pair about a tombstoned intent is redacted on staff's row and the platform's sample too", async () => {
3100
+ const kind = `connector:erasure-${ulid()}`;
3101
+ const erased = dataSubjectId.parse(ulid());
3102
+ const spared = dataSubjectId.parse(ulid());
3103
+ const linked = await routeIntent(kind, erased, 'Anna Ek');
3104
+ const unlinked = await routeIntent(kind, spared, 'Bo Lund');
3105
+ // As the drain files it (`withheldOpsFailure`): the tenant's row says what became of the
3106
+ // intent, staff's row (no tenant, the same scope) quotes the error, marker first.
3107
+ 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})`)));
3108
+ const ops = { linked: op('pair-linked'), unlinked: op('pair-unlinked'), elsewhere: op('pair-elsewhere') };
3109
+ await pair(ops.linked, linked, 'HTTP 409: Anna Ek needs a personal number');
3110
+ await pair(ops.unlinked, unlinked, 'HTTP 409: Bo Lund needs a personal number');
3111
+ // The twin: a staff row naming the same intent under another scope is not this erasure's.
3112
+ await host.admin.recordOpsFailure({
3113
+ actor: staff, operation: ops.elsewhere, stage: 'terminal', tenantId: null, scopeId: s2,
3114
+ message: platformIntentFailureMessage(linked, 'failed: HTTP 409: Anna Ek'),
3115
+ });
3116
+ const rowsOf = async (operation) => (await host.admin.listOpsFailures(staff, { operation })).map((r) => [r.tenantId, r.message]);
3117
+ const before = { linked: await rowsOf(ops.linked), unlinked: await rowsOf(ops.unlinked), elsewhere: await rowsOf(ops.elsewhere) };
3118
+ expect(before.linked[1][1]).toMatch(/^\[upstream reference = up[0-9a-z]+\] platform intent /);
3119
+ expect(await exemplarOwnerOf(ops.linked)).toEqual({ kind: 'platform', tenant: null });
3120
+ await host.admin.shredSubject(staff, t1, s1, erased);
3121
+ // Both rows of the pair: the tenant's by the link it always had, staff's by #2554's.
3122
+ expect(await rowsOf(ops.linked)).toEqual([[t1, REDACTED_FAILURE_NOTE], [null, REDACTED_FAILURE_NOTE]]);
3123
+ expect((await issueOf(ops.linked)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
3124
+ expect(await rowsOf(ops.unlinked)).toEqual(before.unlinked);
3125
+ expect((await issueOf(ops.unlinked)).lastMessage).toBe(before.unlinked[1][1]);
3126
+ expect(await rowsOf(ops.elsewhere)).toEqual(before.elsewhere);
3127
+ await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
3128
+ });
3018
3129
  /** Whose an exemplar is, as the directory stores it — not on the issue read shape. */
3019
3130
  const exemplarOwnerOf = async (operation) => {
3020
3131
  const dump = await host.admin.exportDirectory(staff);
@@ -4340,6 +4451,85 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4340
4451
  expect(both.map((r) => r.code).sort()).toEqual(['permission_denied', null].sort());
4341
4452
  expect(both.map((r) => r.origin).sort()).toEqual(['platform', null].sort());
4342
4453
  });
4454
+ it('#2540: a tenant row with a detail is a pair under one reference, counted once', async () => {
4455
+ const reference = `pair${ulid().toLowerCase()}`;
4456
+ await host.admin.recordOpsFailure({
4457
+ actor: staff,
4458
+ operation: 'contract.paired',
4459
+ stage: 'withheld',
4460
+ tenantId: t1,
4461
+ vertical: 'acme/paired',
4462
+ status: 502,
4463
+ message: `the step failed (reference ${reference})`,
4464
+ detail: 'what the upstream said about something private',
4465
+ reference,
4466
+ });
4467
+ // The tenant's read: its own row, which says what was answered.
4468
+ const own = await host.admin.listOpsFailures(staff, { tenantId: t1, operation: 'contract.paired' });
4469
+ expect(own.map((r) => r.message)).toEqual([`the step failed (reference ${reference})`]);
4470
+ // Staff's read by the reference: both rows, the text on the one with no tenant.
4471
+ const both = await host.admin.listOpsFailures(staff, { reference });
4472
+ expect(both.map((r) => [r.tenantId, r.message])).toEqual([
4473
+ [t1, `the step failed (reference ${reference})`],
4474
+ [null, 'what the upstream said about something private'],
4475
+ ]);
4476
+ // One failure, one count, and the issue's sample is staff's text.
4477
+ const [issue] = await host.admin.listIssues(staff, { operation: 'contract.paired' });
4478
+ expect(issue).toMatchObject({ count: 1, lastMessage: 'what the upstream said about something private' });
4479
+ // A row with no tenant has no tenant row to pair with: `detail` is ignored there.
4480
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.unpaired', message: 'staff text', detail: 'ignored' });
4481
+ const lone = await host.admin.listOpsFailures(staff, { operation: 'contract.unpaired' });
4482
+ expect(lone.map((r) => [r.tenantId, r.message])).toEqual([[null, 'staff text']]);
4483
+ });
4484
+ it('#2540: staff find a pair by the upstream handle its staff row names; an exact search does not', async () => {
4485
+ const reference = `own${ulid().toLowerCase()}`;
4486
+ const upstream = `cf${ulid().toLowerCase()}`;
4487
+ const forged = `cf${ulid().toLowerCase()}`;
4488
+ // A body past the row's bound, which spells a marker of its own for an unrelated id.
4489
+ const body = `internal error; reference = ${upstream} [upstream reference = ${forged}] ${'x'.repeat(2100)}`;
4490
+ await host.admin.recordOpsFailure({
4491
+ actor: staff,
4492
+ operation: 'contract.upstream',
4493
+ tenantId: t1,
4494
+ message: `the step failed (reference ${reference})`,
4495
+ detail: withUpstreamReference(body),
4496
+ reference,
4497
+ });
4498
+ // Bracketed, so an id that only begins this one finds nothing.
4499
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', message: 'x', reference: `${upstream}0` });
4500
+ const bothRows = [
4501
+ [t1, reference],
4502
+ [null, reference],
4503
+ ];
4504
+ const by = async (ref, referenceInText, tenantId) => (await host.admin.listOpsFailures(staff, { reference: ref, referenceInText, ...(tenantId ? { tenantId } : {}) })).map((r) => [r.tenantId, r.reference]);
4505
+ expect(await by(upstream, true)).toEqual(bothRows);
4506
+ expect(await by(reference, true)).toEqual(bothRows);
4507
+ expect(await by(upstream, false)).toEqual([]);
4508
+ expect(await by(upstream.slice(0, -2), true)).toEqual([]);
4509
+ // The row is bounded, and the marker, written first, survives the cut.
4510
+ const [, staffRow] = await host.admin.listOpsFailures(staff, { reference });
4511
+ expect(staffRow.message.length).toBeLessThanOrEqual(2000);
4512
+ expect(staffRow.message.startsWith(`[upstream reference = ${upstream}]`)).toBe(true);
4513
+ // The body's own spelling of a marker finds nothing.
4514
+ expect(await by(forged, true)).toEqual([]);
4515
+ // Narrowed to the tenant, the search lands on the tenant's own row only.
4516
+ expect(await by(reference, false, t1)).toEqual([[t1, reference]]);
4517
+ // A spelled marker ahead of the genuine handle, at the very start or before it: only the
4518
+ // genuine handle makes the marker. A handle past the bound makes none.
4519
+ const real = `cf${ulid().toLowerCase()}`;
4520
+ for (const shape of [`[upstream reference = ${forged}] boom; reference = ${real}`, `boom [Upstream Reference=${forged}] then reference = ${real}`]) {
4521
+ const pair = `shape${ulid().toLowerCase()}`;
4522
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', tenantId: t1, message: 'm', detail: withUpstreamReference(shape), reference: pair });
4523
+ expect((await by(real, true)).map(([, r]) => r)).toContain(pair);
4524
+ expect(await by(forged, true)).toEqual([]);
4525
+ }
4526
+ const long = `long${ulid().toLowerCase()}`;
4527
+ const run = 'a'.repeat(100);
4528
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'contract.upstream', tenantId: t1, message: 'm', detail: withUpstreamReference(`reference = ${run}`), reference: long });
4529
+ const [, unmarked] = await host.admin.listOpsFailures(staff, { reference: long });
4530
+ expect(unmarked.message).toBe(`reference = ${run}`);
4531
+ expect(await by(run, true)).toEqual([]);
4532
+ });
4343
4533
  it('bounds the recorded message — a runaway upstream body never becomes a runaway row', async () => {
4344
4534
  await host.admin.recordOpsFailure({
4345
4535
  actor: staff,
@@ -8585,6 +8775,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
8585
8775
  // #1524: a size read is refused BEFORE it reaches the storage. Addressing a reaped
8586
8776
  // DO to ask its size would recreate an empty database and report that as the scope.
8587
8777
  await expectRefusal(host.admin.scopeDatabaseSize(staff, t3, s), 'conflict', /reaped/);
8778
+ // #2501: the attachment read is refused the same way, never answered as 0.
8779
+ if (host.admin.scopeAttachmentBytes) {
8780
+ await expectRefusal(host.admin.scopeAttachmentBytes(staff, t3, s), 'conflict', /reaped/);
8781
+ }
8588
8782
  // Audited as reapScope against the right scope + actor.
8589
8783
  const reapEntry = (await host.admin.auditLog(staff, { tenantId: t3 })).find((r) => r.action === 'reapScope' && r.scopeId === s);
8590
8784
  expect(reapEntry?.actor).toBe(staff);
@@ -9249,15 +9443,87 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9249
9443
  sampled: 1,
9250
9444
  });
9251
9445
  });
9252
- it("forgets a reaped scope's samples, and never writes one for it again", async () => {
9446
+ it('keeps attachment samples beside the databases, the day\'s latest, until retention, a reaped scope\'s too (#2501)', async () => {
9447
+ const ta = tenantId.parse(ulid());
9448
+ await host.admin.createTenant(staff, { id: ta, slug: 'gauge-attachments', name: 'Gauge Attachments' });
9449
+ const [s, quiet] = [await newScope(ta), await newScope(ta)];
9450
+ const attachmentsOf = async (tenant) => (await host.admin.readMeters(staff, { tenantId: tenant })).perTenant[0].storage;
9451
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '08:00:00'), attachments: 300 }]);
9452
+ // Beside the databases' figure, never inside it, which still names attachments as excluded.
9453
+ expect(await attachmentsOf(ta)).toMatchObject({ bytes: 100, excluded: ['attachments', 'tenant-stores', 'lake'], attachments: { bytes: 300, sampled: 1 } });
9454
+ // A later reading of the day replaces it; an older one, recorded after it, does not.
9455
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '09:00:00'), attachments: 50 }]);
9456
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 100, readAt: day(0, '08:30:00'), attachments: 999 }]);
9457
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 1 });
9458
+ // A reading that did not measure attachments (unsupported, failed, declined) leaves them.
9459
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: s, bytes: 120, readAt: day(0, '10:00:00') }]);
9460
+ expect(await attachmentsOf(ta)).toMatchObject({ bytes: 120, attachments: { bytes: 50, sampled: 1 } });
9461
+ // A scope with none holds 0 of them: a sample like any other, counted in `sampled`.
9462
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: quiet, bytes: 10, readAt: day(0, '10:00:00'), attachments: 0 }]);
9463
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9464
+ // Named under another tenant, refused like the database sample.
9465
+ const other = tenantId.parse(ulid());
9466
+ await host.admin.createTenant(staff, { id: other, slug: 'gauge-attachments-other', name: 'Gauge Attachments Other' });
9467
+ await newScope(other);
9468
+ await host.admin.recordScopeStorage(staff, [{ tenantId: other, scopeId: s, bytes: 1, readAt: day(0, '11:00:00'), attachments: 7 }]);
9469
+ expect((await attachmentsOf(other))?.attachments).toBeUndefined();
9470
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9471
+ // Past retention a sample goes with the database samples, the limit shared across the tables.
9472
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9473
+ ;
9474
+ const old = new Date();
9475
+ old.setUTCMonth(old.getUTCMonth() - 14);
9476
+ const aged = tenantId.parse(ulid());
9477
+ await host.admin.createTenant(staff, { id: aged, slug: 'gauge-attachments-old', name: 'Gauge Attachments Old' });
9478
+ const agedScope = await newScope(aged);
9479
+ await host.admin.recordScopeStorage(staff, [{ tenantId: aged, scopeId: agedScope, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9480
+ expect((await attachmentsOf(aged))?.attachments).toEqual({ bytes: 9, sampled: 1 });
9481
+ expect(await host.admin.pruneScopeStorage(staff, 1)).toBe(1);
9482
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(1);
9483
+ expect((await attachmentsOf(aged))?.attachments).toBeUndefined();
9484
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9485
+ // A reaped scope's attachment samples follow its database days (#2515): the reap keeps
9486
+ // them for the invoice and the gauge, which describes live storage, stops counting them.
9487
+ const reaped = await newScope(ta);
9488
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: reaped, bytes: 1, readAt: old.toISOString(), attachments: 4 }]);
9489
+ await host.admin.recordScopeStorage(staff, [{ tenantId: ta, scopeId: reaped, bytes: 1, readAt: day(0, '12:00:00'), attachments: 3 }]);
9490
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 53, sampled: 3 });
9491
+ await host.admin.archiveScope(staff, ta, reaped);
9492
+ await host.admin.reapScope(staff, ta, reaped);
9493
+ expect((await attachmentsOf(ta))?.attachments).toEqual({ bytes: 50, sampled: 2 });
9494
+ // Retention, not the reap, ends them: the old database day and the old attachment day are
9495
+ // both still there for the prune to take, and the current ones stay.
9496
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9497
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([1]);
9498
+ });
9499
+ it('a tenant reap forgets its attachment samples with its other storage rows (#2501)', async () => {
9500
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9501
+ ;
9502
+ const gone = tenantId.parse(ulid());
9503
+ await host.admin.createTenant(staff, { id: gone, slug: 'gauge-attachments-tenant-reap', name: 'Gauge Attachments Tenant Reap' });
9504
+ const s = await newScope(gone);
9505
+ // Past retention, so the prune (which reads the raw tables) finds the row if the reap left it.
9506
+ const old = new Date();
9507
+ old.setUTCMonth(old.getUTCMonth() - 14);
9508
+ await host.admin.recordScopeStorage(staff, [{ tenantId: gone, scopeId: s, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9509
+ // Its twin, before the reap: both old samples are there for the prune to find.
9510
+ const kept = tenantId.parse(ulid());
9511
+ await host.admin.createTenant(staff, { id: kept, slug: 'gauge-attachments-tenant-kept', name: 'Gauge Attachments Tenant Kept' });
9512
+ const ks = await newScope(kept);
9513
+ await host.admin.recordScopeStorage(staff, [{ tenantId: kept, scopeId: ks, bytes: 1, readAt: old.toISOString(), attachments: 9 }]);
9514
+ await host.admin.setTenantStatus(staff, gone, 'deleting');
9515
+ await host.admin.reapTenant(staff, gone);
9516
+ // Only the kept tenant's two rows (database and attachments) were left to prune.
9517
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9518
+ });
9519
+ it("keeps a reaped scope's days for the invoice, ends its attempts, and never writes one for it again", async () => {
9253
9520
  const tr = tenantId.parse(ulid());
9254
9521
  await host.admin.createTenant(staff, { id: tr, slug: 'gauge-reap', name: 'Gauge Reap' });
9255
9522
  const [kept, reaped] = [await newScope(tr), await newScope(tr)];
9256
9523
  await record(tr, kept, 1, day(0, '02:00:00'));
9257
9524
  await record(tr, reaped, 2, day(-1, '02:00:00'));
9258
9525
  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.
9526
+ // A sample past retention: retention, not the reap, is what ends a reaped scope's days.
9261
9527
  while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9262
9528
  ;
9263
9529
  const old = new Date();
@@ -9265,14 +9531,25 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9265
9531
  expect(await record(tr, reaped, 5, old.toISOString())).toEqual({ recorded: 1 });
9266
9532
  await host.admin.archiveScope(staff, tr, reaped);
9267
9533
  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]);
9534
+ // Right after the reap all three days are there, the old one included: the reap deleted none.
9535
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([5, 2, 3]);
9536
+ // The month it was reaped in is still readable for the tenant, scope by scope.
9537
+ const history = await host.admin.listScopeStorage(staff, { tenantId: tr, since: day(-1, '00:00:00').slice(0, 10) });
9538
+ expect(history.filter((r) => r.scopeId === reaped).map((r) => r.bytes)).toEqual([2, 3]);
9539
+ expect(history.filter((r) => r.scopeId === kept).map((r) => r.bytes)).toEqual([1]);
9540
+ // The current gauge and the attempts describe live storage only.
9541
+ expect((await host.admin.listScopeStorage(staff, { tenantId: tr, latest: true })).map((r) => r.scopeId)).toEqual([kept]);
9542
+ expect((await host.admin.listScopeStorageAttempts(staff)).some((a) => a.scopeId === reaped)).toBe(false);
9270
9543
  expect(await record(tr, reaped, 4, day(0, '03:00:00'))).toEqual({ recorded: 0 });
9271
9544
  expect((await host.admin.readMeters(staff, { tenantId: tr })).perTenant[0].storage).toMatchObject({
9272
9545
  bytes: 1,
9273
9546
  sampled: 1,
9274
9547
  total: 1, // the tombstone is not in the denominator
9275
9548
  });
9549
+ // Retention, not the reap, takes the one past thirteen months, and only it.
9550
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9551
+ ;
9552
+ expect((await host.admin.listScopeStorage(staff, { scopeId: reaped })).map((r) => r.bytes)).toEqual([2, 3]);
9276
9553
  });
9277
9554
  it('keeps an archived scope in the figure and refreshes it, so the tenant is not stuck stale', async () => {
9278
9555
  const ta = tenantId.parse(ulid());
@@ -9375,6 +9652,23 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9375
9652
  await host.deleteSnapshot(staff, tk, fork);
9376
9653
  expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(0);
9377
9654
  });
9655
+ it("deletes a fork's attachment samples with its other samples, and keeps its source's (#2501)", async () => {
9656
+ const tk = tenantId.parse(ulid());
9657
+ await host.admin.createTenant(staff, { id: tk, slug: 'gauge-fork-attachments', name: 'Gauge Fork Attachments' });
9658
+ const source = await newScope(tk);
9659
+ const fork = await host.snapshotScope(staff, tk, source);
9660
+ while ((await host.admin.pruneScopeStorage(staff, 500)) > 0)
9661
+ ;
9662
+ const old = new Date();
9663
+ old.setUTCMonth(old.getUTCMonth() - 14);
9664
+ // Past retention, so the prune (which reads the raw tables) finds whatever the delete left.
9665
+ for (const s of [source, fork]) {
9666
+ await host.admin.recordScopeStorage(staff, [{ tenantId: tk, scopeId: s, bytes: 1, readAt: old.toISOString(), attachments: 5 }]);
9667
+ }
9668
+ await host.deleteSnapshot(staff, tk, fork);
9669
+ // Only the source's database and attachment samples were left to prune: the fork's went with it.
9670
+ expect(await host.admin.pruneScopeStorage(staff, 500)).toBe(2);
9671
+ });
9378
9672
  it('prunes samples older than thirteen months, at most the limit per call', async () => {
9379
9673
  const tp = tenantId.parse(ulid());
9380
9674
  await host.admin.createTenant(staff, { id: tp, slug: 'gauge-prune', name: 'Gauge Prune' });
@@ -9395,6 +9689,882 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9395
9689
  await expect(host.admin.pruneScopeStorage(staff, -1)).rejects.toThrow(/limit/);
9396
9690
  });
9397
9691
  });
9692
+ // -- the daily request rollup (#2501) ----------------------------------------
9693
+ // -- the capability record (RFC wire-signing §7, Q8) ---------------------------
9694
+ describe('the capability record (RFC wire-signing §7, Q8)', () => {
9695
+ // Each test owns its verticals: the records are fleet-wide.
9696
+ const slugOf = (name) => `${name}-${ulid().slice(-8).toLowerCase()}`;
9697
+ const record = (over = {}) => ({
9698
+ envelope: 1,
9699
+ entrySkipped: null,
9700
+ state: 'dual',
9701
+ audience: 'acme-serving',
9702
+ routerKids: ['If4x36FUomE', 'fX5Yi3zNBp0'],
9703
+ platformKids: ['OfcT0KZEJT8'],
9704
+ withheld: null,
9705
+ ...over,
9706
+ });
9707
+ /** A record of an upload that gave no lists, and why. */
9708
+ const withheld = (reason) => record({ state: 'legacy-only', audience: null, routerKids: [], platformKids: [], withheld: reason });
9709
+ const at = (time) => `2026-10-24T${time}.000Z`;
9710
+ /** The whole fleet, one full page: every read names its reach and its limit. */
9711
+ const fleet = { reach: { fleet: true }, limit: LIST_PAGE_MAX };
9712
+ /** Record `rec` for the script `<vertical>-<script>`. */
9713
+ const put = (vertical, script, versionId, rec, time) => host.admin.recordWireAuth(staff, { deploymentRef: `${vertical}-${script}`, verticalSlug: vertical, versionId, record: rec, recordedAt: at(time) });
9714
+ it('keeps one record per deployment, the last upload’s, and lists one vertical’s by deployment', async () => {
9715
+ const [a, b] = [slugOf('wire-a'), slugOf('wire-b')];
9716
+ await put(a, 'serving', 'v1', record({ audience: `${a}-serving` }), '10:00:00');
9717
+ await put(a, 'archive', 'v1', record({ audience: `${a}-archive` }), '10:00:01');
9718
+ await put(b, 'serving', null, withheld('not-in-rollout'), '10:00:02');
9719
+ // The serving script uploaded again, for another version, given nothing this time: its record is replaced.
9720
+ await put(a, 'serving', 'v2', withheld('lists-refused'), '10:05:00');
9721
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([
9722
+ { deploymentRef: `${a}-archive`, verticalSlug: a, versionId: 'v1', record: record({ audience: `${a}-archive` }), recordedAt: at('10:00:01') },
9723
+ { deploymentRef: `${a}-serving`, verticalSlug: a, versionId: 'v2', record: withheld('lists-refused'), recordedAt: at('10:05:00') },
9724
+ ]);
9725
+ const all = await host.admin.listWireAuth(staff, { ...fleet });
9726
+ expect(all.filter((r) => r.verticalSlug === a || r.verticalSlug === b).map((r) => r.deploymentRef)).toEqual([`${a}-archive`, `${a}-serving`, `${b}-serving`]);
9727
+ // An entry the uploader left off is recorded as such: no envelope, and why.
9728
+ await put(b, 'serving', null, record({ envelope: null, entrySkipped: 'an older bundle' }), '10:06:00');
9729
+ expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: b }))[0].record).toMatchObject({ envelope: null, entrySkipped: 'an older bundle' });
9730
+ });
9731
+ it('writes each record to the admin log, naming the vertical, the script and the version, never a key', async () => {
9732
+ const a = slugOf('wire-log');
9733
+ await put(a, 'serving', 'v1', record(), '11:00:00');
9734
+ const rows = (await host.admin.auditLog(staff, { action: 'recordWireAuth' })).filter((r) => r.vertical === a);
9735
+ expect(rows).toHaveLength(1);
9736
+ expect(rows[0].actor).toBe(staff);
9737
+ expect(rows[0].after).toEqual({ vertical: a, deploymentRef: `${a}-serving`, versionId: 'v1', record: record() });
9738
+ });
9739
+ it('refuses a record that does not parse, or contradicts itself, and stores nothing for it', async () => {
9740
+ const a = slugOf('wire-bad');
9741
+ const good = { deploymentRef: `${a}-serving`, verticalSlug: a, versionId: null, record: record(), recordedAt: at('12:00:00') };
9742
+ for (const bad of [
9743
+ { ...good, deploymentRef: '' },
9744
+ { ...good, verticalSlug: '' },
9745
+ { ...good, recordedAt: 'yesterday' },
9746
+ { ...good, record: { ...record(), envelope: 2 } },
9747
+ { ...good, record: { ...record(), state: 'on' } },
9748
+ { ...good, record: { ...record(), withheld: 'because' } },
9749
+ // An envelope beside a reason the entry was left off, or neither.
9750
+ { ...good, record: record({ entrySkipped: 'an older bundle' }) },
9751
+ { ...good, record: record({ envelope: null }) },
9752
+ // Lists given beside a reason none were, lists given naming no key, and a reason with lists still named.
9753
+ { ...good, record: record({ withheld: 'unconfigured' }) },
9754
+ { ...good, record: record({ routerKids: [] }) },
9755
+ { ...good, record: record({ platformKids: [] }) },
9756
+ { ...good, record: { ...withheld('unconfigured'), routerKids: ['If4x36FUomE'] } },
9757
+ { ...good, record: { ...withheld('unconfigured'), state: 'dual' } },
9758
+ ]) {
9759
+ await expectRefusal(host.admin.recordWireAuth(staff, bad), 'validation_failed');
9760
+ }
9761
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
9762
+ });
9763
+ it('reads a page at a time by script name, and each read is access-logged', async () => {
9764
+ const a = slugOf('wire-page');
9765
+ for (const [i, script] of ['a1', 'a2', 'a3'].entries())
9766
+ await put(a, script, null, record(), `14:00:0${i}`);
9767
+ const reach = { fleet: true };
9768
+ const first = await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: 2 });
9769
+ expect(first.map((r) => r.deploymentRef)).toEqual([`${a}-a1`, `${a}-a2`]);
9770
+ const rest = await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: 2, cursor: first.at(-1).deploymentRef });
9771
+ expect(rest.map((r) => r.deploymentRef)).toEqual([`${a}-a3`]);
9772
+ expect((await host.admin.listWireAuth(staff, { reach, verticalSlug: a, order: 'desc', limit: 1 })).map((r) => r.deploymentRef)).toEqual([`${a}-a3`]);
9773
+ // The cap is a full page, and one past it is refused, not clamped.
9774
+ expect(await host.admin.listWireAuth(staff, { reach, verticalSlug: a, limit: LIST_PAGE_MAX })).toHaveLength(3);
9775
+ const reads = (await host.admin.accessLog(staff, { method: 'listWireAuth' }))
9776
+ .map((e) => ({ actor: e.actor, params: JSON.parse(e.params ?? 'null'), count: e.resultCount }))
9777
+ .filter((e) => e.params?.['verticalSlug'] === a);
9778
+ expect(reads).toEqual([
9779
+ { actor: staff, params: { reach, verticalSlug: a, limit: 2 }, count: 2 },
9780
+ { actor: staff, params: { reach, verticalSlug: a, limit: 2, cursor: `${a}-a2` }, count: 1 },
9781
+ { actor: staff, params: { reach, verticalSlug: a, order: 'desc', limit: 1 }, count: 1 },
9782
+ { actor: staff, params: { reach, verticalSlug: a, limit: LIST_PAGE_MAX }, count: 3 },
9783
+ ]);
9784
+ });
9785
+ it('refuses a read that does not name its reach and its limit, and logs no access for it', async () => {
9786
+ const a = slugOf('wire-refuse');
9787
+ await put(a, 'serving', null, record(), '15:00:00');
9788
+ const t = ulid();
9789
+ for (const bad of [
9790
+ // No filter, no reach, an empty reach, both reaches, a reach that is not one.
9791
+ undefined,
9792
+ { limit: 10, verticalSlug: a },
9793
+ { reach: {}, limit: 10, verticalSlug: a },
9794
+ { reach: { tenantId: t, fleet: true }, limit: 10, verticalSlug: a },
9795
+ { reach: { fleet: false }, limit: 10, verticalSlug: a },
9796
+ { reach: { fleet: 'yes' }, limit: 10, verticalSlug: a },
9797
+ { reach: { tenantId: 'acme' }, limit: 10, verticalSlug: a },
9798
+ { reach: { tenantId: '' }, limit: 10, verticalSlug: a },
9799
+ { reach: { tenantId: t, extra: 1 }, limit: 10, verticalSlug: a },
9800
+ { reach: 'fleet', limit: 10, verticalSlug: a },
9801
+ // No limit, or one outside 1..LIST_PAGE_MAX.
9802
+ { reach: { fleet: true }, verticalSlug: a },
9803
+ ...[0, -1, 1.5, LIST_PAGE_MAX + 1, Number.POSITIVE_INFINITY, Number.NaN, '10'].map((limit) => ({ reach: { fleet: true }, limit, verticalSlug: a })),
9804
+ // The optional fields in their own types, and the text ones no longer than their bound.
9805
+ { reach: { fleet: true }, limit: 10, verticalSlug: '' },
9806
+ { reach: { fleet: true }, limit: 10, verticalSlug: 'x'.repeat(257) },
9807
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: 7 },
9808
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: '' },
9809
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, cursor: 'x'.repeat(257) },
9810
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, order: 'sideways' },
9811
+ // Only the filter's own keys: an unknown one is refused, never carried across or logged.
9812
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, tenantId: t },
9813
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, extra: 'x'.repeat(400) },
9814
+ { reach: { fleet: true }, limit: 10, verticalSlug: a, offset: 0 },
9815
+ // Arrays are not a filter, nor a reach.
9816
+ [{ reach: { fleet: true }, limit: 10, verticalSlug: a }],
9817
+ { reach: [{ fleet: true }], limit: 10, verticalSlug: a },
9818
+ ]) {
9819
+ await expectRefusal(host.admin.listWireAuth(staff, bad), 'validation_failed');
9820
+ }
9821
+ const reads = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(a));
9822
+ expect(reads).toEqual([]);
9823
+ });
9824
+ it('confines a tenant’s read to the verticals it owns now', async () => {
9825
+ const [owner, other] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
9826
+ await host.admin.createTenant(staff, { id: owner, slug: `wire-own-${owner.slice(-6).toLowerCase()}`, name: 'Wire Owner' });
9827
+ await host.admin.createTenant(staff, { id: other, slug: `wire-oth-${other.slice(-6).toLowerCase()}`, name: 'Wire Other' });
9828
+ const [mine, theirs, platform, unregistered] = [slugOf('wire-mine'), slugOf('wire-theirs'), slugOf('wire-plat'), slugOf('wire-unreg')];
9829
+ await host.admin.registerVertical(staff, { slug: mine, name: 'Mine', source: 'cli', ownerTenant: owner });
9830
+ await host.admin.registerVertical(staff, { slug: theirs, name: 'Theirs', source: 'cli', ownerTenant: other });
9831
+ await host.admin.registerVertical(staff, { slug: platform, name: 'Platform', source: 'cli' });
9832
+ for (const [i, slug] of [mine, theirs, platform, unregistered].entries()) {
9833
+ await put(slug, 'serving', null, record(), `16:00:0${i}`);
9834
+ await put(slug, 'archive', 'v1', record(), `16:00:1${i}`);
9835
+ }
9836
+ const refs = async (reach, verticalSlug) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, ...(verticalSlug ? { verticalSlug } : {}) }))
9837
+ .map((r) => r.deploymentRef)
9838
+ .filter((ref) => [mine, theirs, platform, unregistered].some((slug) => ref.startsWith(`${slug}-`)));
9839
+ expect(await refs({ tenantId: owner })).toEqual([`${mine}-archive`, `${mine}-serving`]);
9840
+ expect(await refs({ tenantId: other })).toEqual([`${theirs}-archive`, `${theirs}-serving`]);
9841
+ // Naming another tenant's vertical inside one tenant's reach reaches nothing, not that vertical.
9842
+ expect(await refs({ tenantId: owner }, theirs)).toEqual([]);
9843
+ expect(await refs({ tenantId: owner }, mine)).toEqual([`${mine}-archive`, `${mine}-serving`]);
9844
+ // A tenant that owns nothing reaches nothing; a vertical no tenant owns, or the registry does not
9845
+ // know, is reached by the fleet alone.
9846
+ expect(await refs({ tenantId: tenantId.parse(ulid()) })).toEqual([]);
9847
+ for (const slug of [mine, theirs, platform, unregistered]) {
9848
+ expect(await refs({ fleet: true }, slug)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9849
+ }
9850
+ // A page inside the reach pages over the tenant's records only.
9851
+ const page = await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1 });
9852
+ expect(page.map((r) => r.deploymentRef)).toEqual([`${mine}-archive`]);
9853
+ const next = await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1, cursor: page[0].deploymentRef });
9854
+ expect(next.map((r) => r.deploymentRef)).toEqual([`${mine}-serving`]);
9855
+ expect(await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: 1, cursor: next[0].deploymentRef })).toEqual([]);
9856
+ });
9857
+ it('reads only the filter’s own fields, and echoes an unknown key no longer than its bound', async () => {
9858
+ const a = slugOf('wire-own');
9859
+ await put(a, 'archive', null, record(), '19:00:00');
9860
+ await put(a, 'serving', null, record(), '19:00:01');
9861
+ const refs = (rows) => rows.map((r) => r.deploymentRef);
9862
+ // An inherited cursor or order is not the caller's: it is neither applied nor judged, and the
9863
+ // judged copy that is logged carries none of it.
9864
+ const inherited = Object.assign(Object.create({ cursor: `${a}-serving`, order: 'sideways' }), {
9865
+ reach: { fleet: true },
9866
+ limit: LIST_PAGE_MAX,
9867
+ verticalSlug: a,
9868
+ });
9869
+ expect(refs(await host.admin.listWireAuth(staff, inherited))).toEqual([`${a}-archive`, `${a}-serving`]);
9870
+ const logged = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(a));
9871
+ expect(logged.map((e) => e.params)).toEqual([JSON.stringify({ reach: { fleet: true }, verticalSlug: a, limit: LIST_PAGE_MAX })]);
9872
+ // A reach or a limit only inherited is no reach or limit at all, and an inherited tenant is no tenant.
9873
+ for (const bad of [
9874
+ Object.create({ reach: { fleet: true }, limit: 10 }),
9875
+ Object.assign(Object.create({ limit: 10 }), { reach: { fleet: true }, verticalSlug: a }),
9876
+ Object.assign(Object.create({ reach: { fleet: true } }), { limit: 10, verticalSlug: a }),
9877
+ { reach: Object.create({ tenantId: ulid() }), limit: 10, verticalSlug: a },
9878
+ { reach: Object.create({ fleet: true }), limit: 10, verticalSlug: a },
9879
+ ]) {
9880
+ await expectRefusal(host.admin.listWireAuth(staff, bad), 'validation_failed');
9881
+ }
9882
+ // An unknown key is named in the refusal, cut to its bound with an ellipsis.
9883
+ const longKey = 'k'.repeat(400);
9884
+ const error = await host.admin.listWireAuth(staff, { reach: { fleet: true }, limit: 10, [longKey]: 1 }).then(() => null, (e) => e);
9885
+ expect(errorCodeOf(error)).toBe('validation_failed');
9886
+ const message = error.message;
9887
+ expect(message).toContain(`${'k'.repeat(64)}…`);
9888
+ expect(message).not.toContain('k'.repeat(65));
9889
+ });
9890
+ it('logs a tenant’s read against that tenant and the fleet’s against none, the judged filter with its reach first', async () => {
9891
+ const owner = tenantId.parse(ulid());
9892
+ await host.admin.createTenant(staff, { id: owner, slug: `wire-log-${owner.slice(-6).toLowerCase()}`, name: 'Wire Log' });
9893
+ const mine = slugOf('wire-logged');
9894
+ await host.admin.registerVertical(staff, { slug: mine, name: 'Logged', source: 'cli', ownerTenant: owner });
9895
+ await put(mine, 'serving', null, record(), '17:00:00');
9896
+ // The reach given last, as a caller may spell it: the log still names it first.
9897
+ await host.admin.listWireAuth(staff, { limit: 5, verticalSlug: mine, reach: { tenantId: owner } });
9898
+ const forTenant = await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner });
9899
+ expect(forTenant.map((e) => ({ tenantId: e.tenantId, params: e.params, count: e.resultCount }))).toEqual([
9900
+ { tenantId: owner, params: JSON.stringify({ reach: { tenantId: owner }, verticalSlug: mine, limit: 5 }), count: 1 },
9901
+ ]);
9902
+ // The fleet's read is logged against no tenant, and is told apart by its reach.
9903
+ await host.admin.listWireAuth(staff, { reach: { fleet: true }, limit: 5, verticalSlug: mine });
9904
+ const fleetRows = (await host.admin.accessLog(staff, { method: 'listWireAuth' })).filter((e) => (e.params ?? '').includes(mine) && e.tenantId === null);
9905
+ expect(fleetRows.map((e) => e.params)).toEqual([JSON.stringify({ reach: { fleet: true }, verticalSlug: mine, limit: 5 })]);
9906
+ expect(await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner })).toHaveLength(1);
9907
+ // A vertical and a cursor at their longest cannot push the reach out of the logged text, which
9908
+ // the log keeps cut to a bounded length.
9909
+ const long = 'z'.repeat(WIRE_AUTH_FILTER_TEXT_MAX);
9910
+ await host.admin.listWireAuth(staff, { cursor: long, verticalSlug: long, limit: 5, reach: { tenantId: owner } });
9911
+ const logged = (await host.admin.accessLog(staff, { method: 'listWireAuth', tenantId: owner })).map((e) => e.params ?? '');
9912
+ expect(logged).toHaveLength(2);
9913
+ for (const params of logged)
9914
+ expect(params.startsWith(`{"reach":{"tenantId":"${owner}"}`)).toBe(true);
9915
+ });
9916
+ it('follows the vertical’s current owner, resolved when the read runs', async () => {
9917
+ const [first, second] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
9918
+ await host.admin.createTenant(staff, { id: first, slug: `wire-fst-${first.slice(-6).toLowerCase()}`, name: 'Wire First' });
9919
+ await host.admin.createTenant(staff, { id: second, slug: `wire-snd-${second.slice(-6).toLowerCase()}`, name: 'Wire Second' });
9920
+ const slug = slugOf('wire-owned');
9921
+ const refsOf = async (owner) => (await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => r.deploymentRef);
9922
+ // A record written while no tenant owned the slug is read by whoever owns it when the read runs.
9923
+ await put(slug, 'archive', 'v0', record(), '18:00:00');
9924
+ expect(await refsOf(first)).toEqual([]);
9925
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
9926
+ expect(await refsOf(first)).toEqual([`${slug}-archive`]);
9927
+ await put(slug, 'serving', 'v1', record(), '18:00:01');
9928
+ expect(await refsOf(first)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9929
+ // An owner changes by deleting the vertical, which takes its records, and registering it again:
9930
+ // the tenant that held it reads nothing after, the new owner reads only what was recorded since.
9931
+ await host.admin.deleteVertical(staff, slug);
9932
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: second });
9933
+ expect(await refsOf(first)).toEqual([]);
9934
+ expect(await refsOf(second)).toEqual([]);
9935
+ await put(slug, 'serving', 'v2', record(), '18:00:02');
9936
+ expect(await refsOf(first)).toEqual([]);
9937
+ expect(await refsOf(second)).toEqual([`${slug}-serving`]);
9938
+ });
9939
+ it('forgets a vertical’s records with the vertical', async () => {
9940
+ const a = slugOf('wire-del');
9941
+ await host.admin.registerVertical(staff, { slug: a, name: 'Wire record', source: 'cli' });
9942
+ await put(a, 'serving', null, record(), '13:00:00');
9943
+ const kept = slugOf('wire-kept');
9944
+ await put(kept, 'serving', null, record(), '13:00:01');
9945
+ await host.admin.deleteVertical(staff, a);
9946
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
9947
+ // Another vertical's record is not touched.
9948
+ expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: kept })).toHaveLength(1);
9949
+ });
9950
+ });
9951
+ describe('the daily request rollup (#2501)', () => {
9952
+ // Each test owns its days, because the rollup's record of a day is fleet-wide.
9953
+ const dayAgo = (n) => new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10);
9954
+ const monthsAgo = (m) => {
9955
+ const d = new Date();
9956
+ d.setUTCMonth(d.getUTCMonth() - m);
9957
+ return d.toISOString().slice(0, 10);
9958
+ };
9959
+ const at = (day, time) => `${day}T${time}.000Z`;
9960
+ const ids = () => ({ tenant: tenantId.parse(ulid()), scope: scopeId.parse(ulid()) });
9961
+ // `n` requests, all served (2xx) unless a split is given.
9962
+ const served = (n, split = {}) => ({ requests: n, class2xx: n, class3xx: 0, class4xx: 0, class5xx: 0, rateLimited: 0, ...split });
9963
+ 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 () => {
9964
+ const day = dayAgo(2);
9965
+ const [a, b] = [ids(), ids()];
9966
+ // 40 routed: 30 served, 1 redirected, 7 refused (5 of them the router's rate limit), 2 failed.
9967
+ const split = { class2xx: 30, class3xx: 1, class4xx: 7, class5xx: 2, rateLimited: 5 };
9968
+ expect(await host.admin.recordRequestDay(staff, {
9969
+ day,
9970
+ attemptedAt: at(day, '23:00:00'),
9971
+ counts: [
9972
+ { tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(40, split), sampled: true },
9973
+ // A request that resolved to no vertical is kept under none.
9974
+ { tenantId: a.tenant, vertical: null, scopeId: a.scope, ...served(2), sampled: false },
9975
+ { tenantId: b.tenant, vertical: 'acme-crm', scopeId: b.scope, ...served(5), sampled: false },
9976
+ ],
9977
+ })).toEqual({ recorded: 3 });
9978
+ expect(await host.admin.listRequestDays(staff, { tenantId: a.tenant })).toEqual([
9979
+ { day, tenantId: a.tenant, vertical: null, scopeId: a.scope, ...served(2), sampled: false, estimated: true },
9980
+ { day, tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(40, split), sampled: true, estimated: true },
9981
+ ]);
9982
+ // Read again, the day is what the newer read counted: a key it no longer names is gone.
9983
+ await host.admin.recordRequestDay(staff, {
9984
+ day,
9985
+ attemptedAt: at(day, '23:30:00'),
9986
+ counts: [{ tenantId: a.tenant, vertical: 'acme-crm', scopeId: a.scope, ...served(41), sampled: true }],
9987
+ });
9988
+ expect((await host.admin.listRequestDays(staff, { since: day, until: day })).map((r) => [r.tenantId, r.requests])).toEqual([
9989
+ [a.tenant, 41],
9990
+ ]);
9991
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
9992
+ {
9993
+ day,
9994
+ attemptedAt: at(day, '23:30:00'),
9995
+ readAt: at(day, '23:30:00'),
9996
+ rows: 1,
9997
+ error: null,
9998
+ saturated: false,
9999
+ failingSince: null,
10000
+ failingCount: null,
10001
+ },
10002
+ ]);
10003
+ // K-24: both reads are attributable.
10004
+ const reads = await host.admin.accessLog(staff, { method: 'listRequestDays' });
10005
+ expect(reads.some((r) => r.tenantId === a.tenant)).toBe(true);
10006
+ expect((await host.admin.accessLog(staff, { method: 'listRequestRollupDays' })).length).toBeGreaterThan(0);
10007
+ });
10008
+ it('records a failed read as the attempt, writes nothing of the day, and keeps a read that succeeded', async () => {
10009
+ const day = dayAgo(3);
10010
+ const t = ids();
10011
+ expect(await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '01:00:00'), error: 'HTTP 503' })).toEqual({
10012
+ recorded: 0,
10013
+ });
10014
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10015
+ // A failure named no run, so it starts one of its own.
10016
+ {
10017
+ day,
10018
+ attemptedAt: at(day, '01:00:00'),
10019
+ readAt: null,
10020
+ rows: null,
10021
+ error: 'HTTP 503',
10022
+ saturated: false,
10023
+ failingSince: at(day, '01:00:00'),
10024
+ failingCount: 1,
10025
+ },
10026
+ ]);
10027
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([]);
10028
+ // A success clears the error, and ends the run the failure had started.
10029
+ await host.admin.recordRequestDay(staff, {
10030
+ day,
10031
+ attemptedAt: at(day, '02:00:00'),
10032
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(9), sampled: false }],
10033
+ });
10034
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10035
+ readAt: at(day, '02:00:00'),
10036
+ error: null,
10037
+ failingSince: null,
10038
+ failingCount: null,
10039
+ });
10040
+ // A later failure records the attempt and leaves the day's rows and its successful read. It
10041
+ // names the run it extends, and the record keeps that run's start and length.
10042
+ await host.admin.recordRequestDay(staff, {
10043
+ day,
10044
+ attemptedAt: at(day, '03:00:00'),
10045
+ error: 'HTTP 503',
10046
+ failingSince: at(day, '00:30:00'),
10047
+ failingCount: 4,
10048
+ });
10049
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10050
+ {
10051
+ day,
10052
+ attemptedAt: at(day, '03:00:00'),
10053
+ readAt: at(day, '02:00:00'),
10054
+ rows: 1,
10055
+ error: 'HTTP 503',
10056
+ saturated: false,
10057
+ failingSince: at(day, '00:30:00'),
10058
+ failingCount: 4,
10059
+ },
10060
+ ]);
10061
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), error: 'HTTP 503', failingCount: 0 })).rejects.toThrow(/failingCount/);
10062
+ expect((await host.admin.listRequestDays(staff, { since: day, until: day })).map((r) => r.requests)).toEqual([9]);
10063
+ });
10064
+ it('records a saturated day as a standing state on its record, and a later read clears it', async () => {
10065
+ const day = dayAgo(8);
10066
+ const t = ids();
10067
+ await host.admin.recordRequestDay(staff, {
10068
+ day,
10069
+ attemptedAt: at(day, '01:00:00'),
10070
+ error: 'more than 50000 groups',
10071
+ saturated: true,
10072
+ failingSince: at(day, '00:00:00'),
10073
+ failingCount: 3,
10074
+ });
10075
+ // Saturated belongs to no run of failures, even when it is handed one.
10076
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([
10077
+ {
10078
+ day,
10079
+ attemptedAt: at(day, '01:00:00'),
10080
+ readAt: null,
10081
+ rows: null,
10082
+ error: 'more than 50000 groups',
10083
+ saturated: true,
10084
+ failingSince: null,
10085
+ failingCount: null,
10086
+ },
10087
+ ]);
10088
+ // Its twin: an ordinary failure of the same day is not saturated, and does start a run.
10089
+ await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '02:00:00'), error: 'HTTP 503' });
10090
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10091
+ saturated: false,
10092
+ failingCount: 1,
10093
+ });
10094
+ await host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '03:00:00'), error: 'too many', saturated: true });
10095
+ await host.admin.recordRequestDay(staff, {
10096
+ day,
10097
+ attemptedAt: at(day, '04:00:00'),
10098
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(2), sampled: false }],
10099
+ });
10100
+ expect((await host.admin.listRequestRollupDays(staff, { since: day, until: day }))[0]).toMatchObject({
10101
+ readAt: at(day, '04:00:00'),
10102
+ error: null,
10103
+ saturated: false,
10104
+ failingSince: null,
10105
+ failingCount: null,
10106
+ });
10107
+ });
10108
+ it('refuses a malformed count whole, writing none of the day; the same day well formed is written', async () => {
10109
+ const day = dayAgo(4);
10110
+ const t = ids();
10111
+ const good = { tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(3), sampled: false };
10112
+ await expect(host.admin.recordRequestDay(staff, {
10113
+ day,
10114
+ attemptedAt: at(day, '04:00:00'),
10115
+ counts: [good, { ...good, tenantId: 'not-a-tenant-id', ...served(1) }],
10116
+ })).rejects.toThrow();
10117
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), counts: [good, { ...good, requests: -1 }] })).rejects.toThrow();
10118
+ // A split that does not fit inside its total, or 429s beyond the 4xx, is malformed too.
10119
+ await expect(host.admin.recordRequestDay(staff, { day, attemptedAt: at(day, '04:00:00'), counts: [good, { ...good, ...served(3, { class5xx: 1 }) }] })).rejects.toThrow();
10120
+ await expect(host.admin.recordRequestDay(staff, {
10121
+ day,
10122
+ attemptedAt: at(day, '04:00:00'),
10123
+ counts: [good, { ...good, ...served(3, { class2xx: 2, class4xx: 1, rateLimited: 2 }) }],
10124
+ })).rejects.toThrow();
10125
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([]);
10126
+ expect(await host.admin.listRequestRollupDays(staff, { since: day, until: day })).toEqual([]);
10127
+ // Two counts for one key are summed, every column of them, never one dropped. Each column
10128
+ // is non-zero on both sides and differs between them, so a column taken from one side
10129
+ // instead of summed cannot pass for the sum.
10130
+ const one = served(4, { class2xx: 1, class3xx: 1, class4xx: 1, class5xx: 1, rateLimited: 1 });
10131
+ const other = served(10, { class2xx: 2, class3xx: 3, class4xx: 2, class5xx: 3, rateLimited: 2 });
10132
+ expect(await host.admin.recordRequestDay(staff, {
10133
+ day,
10134
+ attemptedAt: at(day, '05:00:00'),
10135
+ counts: [
10136
+ { ...good, ...one },
10137
+ { ...good, ...other, sampled: true },
10138
+ ],
10139
+ })).toEqual({ recorded: 1 });
10140
+ expect(await host.admin.listRequestDays(staff, { since: day, until: day })).toEqual([
10141
+ {
10142
+ day,
10143
+ tenantId: t.tenant,
10144
+ vertical: 'acme-crm',
10145
+ scopeId: t.scope,
10146
+ ...served(14, { class2xx: 3, class3xx: 4, class4xx: 3, class5xx: 4, rateLimited: 3 }),
10147
+ sampled: true,
10148
+ estimated: true,
10149
+ },
10150
+ ]);
10151
+ });
10152
+ it('filters by tenant, vertical, scope and an inclusive window of days, and bounds the page', async () => {
10153
+ const [d5, d6, d7] = [dayAgo(5), dayAgo(6), dayAgo(7)];
10154
+ const t = tenantId.parse(ulid());
10155
+ const [s1, s2] = [scopeId.parse(ulid()), scopeId.parse(ulid())];
10156
+ for (const day of [d5, d6, d7]) {
10157
+ await host.admin.recordRequestDay(staff, {
10158
+ day,
10159
+ attemptedAt: at(day, '23:59:00'),
10160
+ counts: [
10161
+ { tenantId: t, vertical: 'acme-crm', scopeId: s1, ...served(1), sampled: false },
10162
+ { tenantId: t, vertical: 'acme-desk', scopeId: s2, ...served(2), sampled: false },
10163
+ ],
10164
+ });
10165
+ }
10166
+ expect(await host.admin.listRequestDays(staff, { tenantId: t })).toHaveLength(6);
10167
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, since: d6 })).map((r) => r.day)).toEqual([d6, d6, d5, d5]);
10168
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, until: d6 })).map((r) => r.day)).toEqual([d7, d7, d6, d6]);
10169
+ expect((await host.admin.listRequestDays(staff, { tenantId: t, vertical: 'acme-desk' })).map((r) => r.scopeId)).toEqual([s2, s2, s2]);
10170
+ expect(await host.admin.listRequestDays(staff, { tenantId: t, scopeId: s1, since: d5 })).toHaveLength(1);
10171
+ expect(await host.admin.listRequestDays(staff, { tenantId: t, limit: 2 })).toHaveLength(2);
10172
+ await expect(host.admin.listRequestDays(staff, { limit: 0 })).rejects.toThrow(/limit/);
10173
+ await expect(host.admin.listRequestDays(staff, { since: '2026-13-01' })).rejects.toThrow(/UTC day/);
10174
+ });
10175
+ it('prunes days older than an explicit horizon, at most the limit per call; a null horizon prunes nothing', async () => {
10176
+ const t = ids();
10177
+ const days = [monthsAgo(16), monthsAgo(15), monthsAgo(14), monthsAgo(2), dayAgo(1)];
10178
+ for (const day of days) {
10179
+ await host.admin.recordRequestDay(staff, {
10180
+ day,
10181
+ attemptedAt: at(day, '12:00:00'),
10182
+ counts: [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(1), sampled: false }],
10183
+ });
10184
+ }
10185
+ // The default keeps everything: nothing goes, however old.
10186
+ expect(await host.admin.pruneRequestDays(staff, 500, null)).toBe(0);
10187
+ expect(await host.admin.listRequestDays(staff, { tenantId: t.tenant })).toHaveLength(5);
10188
+ // Its positive twin: thirteen months named, the three older days go, two per call.
10189
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(2);
10190
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(1);
10191
+ expect(await host.admin.pruneRequestDays(staff, 2, 13)).toBe(0);
10192
+ expect((await host.admin.listRequestDays(staff, { tenantId: t.tenant })).map((r) => r.day)).toEqual([monthsAgo(2), dayAgo(1)]);
10193
+ // The record of a pruned day goes with it; a kept day's stays.
10194
+ const recorded = (await host.admin.listRequestRollupDays(staff)).map((r) => r.day);
10195
+ expect(recorded).not.toContain(monthsAgo(14));
10196
+ expect(recorded).toEqual(expect.arrayContaining([monthsAgo(2), dayAgo(1)]));
10197
+ await expect(host.admin.pruneRequestDays(staff, 0, 13)).rejects.toThrow(/limit/);
10198
+ await expect(host.admin.pruneRequestDays(staff, 10, 0)).rejects.toThrow(/whole number of months/);
10199
+ });
10200
+ it('the sweep phase rolls up due days into this directory, stops at a failed day, and retries it only after an hour', async () => {
10201
+ // Days far from every other test's, so the fleet-wide record of a day is this test's alone.
10202
+ const now = '2031-05-10T12:00:00.000Z';
10203
+ const t = ids();
10204
+ const asked = [];
10205
+ let failing = '2031-05-08';
10206
+ const options = (nowIso) => ({
10207
+ actor: staff,
10208
+ fetch: connectorTestFetch,
10209
+ sweepers: {},
10210
+ drainRetries: false,
10211
+ requestRollup: {
10212
+ lookbackDays: 3,
10213
+ now: () => nowIso,
10214
+ read: async (day) => {
10215
+ asked.push(day);
10216
+ if (day === failing)
10217
+ throw new Error('Cloudflare Analytics Engine query failed: HTTP 503');
10218
+ return [{ tenantId: t.tenant, vertical: 'acme-crm', scopeId: t.scope, ...served(Number(day.slice(-2))), sampled: false }];
10219
+ },
10220
+ },
10221
+ });
10222
+ // The failed day ends the pass's reads: the day after it is left for the next pass.
10223
+ const first = await runPlatformSweep(host, options(now));
10224
+ expect(asked).toEqual(['2031-05-07', '2031-05-08']);
10225
+ expect(first.requestRollup).toMatchObject({ due: 3, read: 1, failed: 1, recorded: 1 });
10226
+ expect(first.errors.filter((e) => e.kind === 'request-rollup')).toEqual([
10227
+ { kind: 'request-rollup', id: '2031-05-08', error: expect.stringMatching(/HTTP 503/) },
10228
+ ]);
10229
+ expect((await host.admin.listRequestDays(staff, { tenantId: t.tenant })).map((r) => [r.day, r.requests])).toEqual([
10230
+ ['2031-05-07', 7],
10231
+ ]);
10232
+ // Inside the hour the failed day is not read again; the day after it is.
10233
+ failing = '';
10234
+ const soon = await runPlatformSweep(host, options('2031-05-10T12:30:00.000Z'));
10235
+ expect(soon.requestRollup).toMatchObject({ due: 1, read: 1 });
10236
+ expect(asked.slice(2)).toEqual(['2031-05-09']);
10237
+ // An hour on, the failed day is read, and only it.
10238
+ const later = await runPlatformSweep(host, options('2031-05-10T13:00:00.000Z'));
10239
+ expect(later.requestRollup).toMatchObject({ due: 1, read: 1, failed: 0, recorded: 1 });
10240
+ expect(asked.slice(3)).toEqual(['2031-05-08']);
10241
+ expect((await host.admin.listRequestRollupDays(staff, { since: '2031-05-07', until: '2031-05-09' })).map((r) => [r.day, r.error])).toEqual([
10242
+ ['2031-05-07', null],
10243
+ ['2031-05-08', null],
10244
+ ['2031-05-09', null],
10245
+ ]);
10246
+ });
10247
+ });
10248
+ // -- the daily meter snapshot (#2501) -----------------------------------------
10249
+ describe('the daily meter snapshot (#2501)', () => {
10250
+ // A snapshot reads the whole directory, which every other test also writes to, so each test
10251
+ // narrows its reads to its own tenants. The record of a day is fleet-wide, so each test owns its
10252
+ // days: far from today, and from each other's.
10253
+ const newTenant = async () => {
10254
+ const t = tenantId.parse(ulid());
10255
+ await host.admin.createTenant(staff, { id: t, slug: `meter-day-${t.toLowerCase()}`, name: 'Meter day' });
10256
+ return t;
10257
+ };
10258
+ const newScope = async (tenant, vertical) => {
10259
+ const s = scopeId.parse(ulid());
10260
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: s, ...(vertical ? { vertical } : {}) });
10261
+ await host.admin.activateScope(staff, tenant, s);
10262
+ return s;
10263
+ };
10264
+ const counts = (c) => ({
10265
+ total: 0,
10266
+ active: 0,
10267
+ suspended: 0,
10268
+ provisioning: 0,
10269
+ archived: 0,
10270
+ reaped: 0,
10271
+ ...c,
10272
+ });
10273
+ const monthsAgo = (m) => {
10274
+ const d = new Date();
10275
+ d.setUTCMonth(d.getUTCMonth() - m);
10276
+ return d.toISOString().slice(0, 10);
10277
+ };
10278
+ 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 () => {
10279
+ const at = '2032-03-04T00:07:00.000Z';
10280
+ const t = await newTenant();
10281
+ await newScope(t, 'meter-crm');
10282
+ await newScope(t, 'meter-crm');
10283
+ const archived = await newScope(t, 'meter-crm');
10284
+ await newScope(t, 'meter-desk');
10285
+ await newScope(t);
10286
+ await host.admin.archiveScope(staff, t, archived);
10287
+ await host.admin.grantEntitlement(staff, t, 'meter-sku', { plan: 'pro' });
10288
+ // Lapsed AT the instant: a grant is live only while its expiry is after the reading.
10289
+ await host.admin.grantEntitlement(staff, t, 'meter-aux', { expiresAt: at });
10290
+ // Its twin, a millisecond later, is live.
10291
+ await host.admin.grantEntitlement(staff, t, 'meter-trial', { expiresAt: '2032-03-04T00:07:00.001Z' });
10292
+ // A suspended tenant's active scope is counted suspended, and the tenant bills nothing.
10293
+ const off = await newTenant();
10294
+ await newScope(off, 'meter-crm');
10295
+ await host.admin.setTenantStatus(staff, off, 'suspended');
10296
+ // A reaped tenant is a tombstone: no row. Its twin, a tenant being deleted, has one.
10297
+ const [gone, going] = [await newTenant(), await newTenant()];
10298
+ await host.admin.setTenantStatus(staff, gone, 'deleting');
10299
+ await host.admin.reapTenant(staff, gone);
10300
+ await host.admin.setTenantStatus(staff, going, 'deleting');
10301
+ const taken = await host.admin.recordMeterDay(staff, { at });
10302
+ expect(taken).toMatchObject({ day: '2032-03-04', taken: true });
10303
+ const fleet = await host.admin.listMeterDays(staff, { since: '2032-03-04', until: '2032-03-04', limit: 10_000 });
10304
+ expect(taken.tenants).toBe(fleet.length);
10305
+ expect(fleet.map((r) => r.tenantId)).not.toContain(gone);
10306
+ expect(fleet.find((r) => r.tenantId === going)).toMatchObject({ status: 'deleting', billable: false, scopes: counts({}) });
10307
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([
10308
+ {
10309
+ day: '2032-03-04',
10310
+ tenantId: t,
10311
+ readAt: at,
10312
+ status: 'active',
10313
+ billable: true,
10314
+ scopes: counts({ total: 5, active: 4, archived: 1 }),
10315
+ // Null first, then by vertical; the split sums to the tenant's counts.
10316
+ verticals: [
10317
+ { vertical: null, ...counts({ total: 1, active: 1 }) },
10318
+ { vertical: 'meter-crm', ...counts({ total: 3, active: 2, archived: 1 }) },
10319
+ { vertical: 'meter-desk', ...counts({ total: 1, active: 1 }) },
10320
+ ],
10321
+ entitlements: [
10322
+ { entitlementKey: 'meter-aux', plan: null, expiresAt: at, live: false },
10323
+ { entitlementKey: 'meter-sku', plan: 'pro', expiresAt: null, live: true },
10324
+ { entitlementKey: 'meter-trial', plan: null, expiresAt: '2032-03-04T00:07:00.001Z', live: true },
10325
+ ],
10326
+ },
10327
+ ]);
10328
+ expect((await host.admin.listMeterDays(staff, { tenantId: off }))[0]).toMatchObject({
10329
+ status: 'suspended',
10330
+ billable: false,
10331
+ scopes: counts({ total: 1, suspended: 1 }),
10332
+ verticals: [{ vertical: 'meter-crm', ...counts({ total: 1, suspended: 1 }) }],
10333
+ });
10334
+ expect(await host.admin.listMeterDayTakes(staff, { since: '2032-03-04', until: '2032-03-04' })).toEqual([
10335
+ {
10336
+ day: '2032-03-04',
10337
+ attemptedAt: at,
10338
+ takenAt: at,
10339
+ tenants: fleet.length,
10340
+ error: null,
10341
+ failingSince: null,
10342
+ failingCount: null,
10343
+ },
10344
+ ]);
10345
+ // K-24: both reads are attributable, the first to the tenant it narrowed to.
10346
+ expect((await host.admin.accessLog(staff, { method: 'listMeterDays' })).some((r) => r.tenantId === t)).toBe(true);
10347
+ expect((await host.admin.accessLog(staff, { method: 'listMeterDayTakes' })).length).toBeGreaterThan(0);
10348
+ });
10349
+ it('agrees with /meters read at the same moment', async () => {
10350
+ const t = await newTenant();
10351
+ await newScope(t, 'meter-crm');
10352
+ const archived = await newScope(t);
10353
+ await host.admin.archiveScope(staff, t, archived);
10354
+ await host.admin.grantEntitlement(staff, t, 'meter-sku', { plan: 'pro' });
10355
+ await host.admin.grantEntitlement(staff, t, 'meter-old', { expiresAt: new Date(Date.now() - 86_400_000).toISOString() });
10356
+ const live = (await host.admin.readMeters(staff, { tenantId: t })).perTenant[0];
10357
+ await host.admin.recordMeterDay(staff, { at: new Date().toISOString() });
10358
+ const [kept] = await host.admin.listMeterDays(staff, { tenantId: t });
10359
+ expect(kept.scopes).toEqual(live.scopes);
10360
+ expect(kept.billable).toBe(live.billable);
10361
+ expect({
10362
+ live: kept.entitlements.filter((g) => g.live).length,
10363
+ expired: kept.entitlements.filter((g) => !g.live).length,
10364
+ }).toEqual(live.entitlements);
10365
+ });
10366
+ it('the first snapshot of a day stands: a later attempt that day writes nothing, and a failure never overwrites it', async () => {
10367
+ const t = await newTenant();
10368
+ await newScope(t);
10369
+ const first = '2032-04-01T00:05:00.000Z';
10370
+ expect(await host.admin.recordMeterDay(staff, { at: first })).toMatchObject({ taken: true });
10371
+ await newScope(t);
10372
+ expect(await host.admin.recordMeterDay(staff, { at: '2032-04-01T00:20:00.000Z' })).toEqual({
10373
+ day: '2032-04-01',
10374
+ taken: false,
10375
+ tenants: 0,
10376
+ });
10377
+ // A failure recorded after the snapshot committed (its caller stopped waiting) is not the day's record.
10378
+ await host.admin.recordMeterDay(staff, { at: '2032-04-01T00:35:00.000Z', error: 'took longer than 30000 ms' });
10379
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([
10380
+ expect.objectContaining({ readAt: first, scopes: counts({ total: 1, active: 1 }) }),
10381
+ ]);
10382
+ expect((await host.admin.listMeterDayTakes(staff, { since: '2032-04-01', until: '2032-04-01' }))[0]).toMatchObject({
10383
+ attemptedAt: first,
10384
+ takenAt: first,
10385
+ error: null,
10386
+ });
10387
+ // Its positive twin: the next day is a new snapshot, and sees the second scope.
10388
+ expect(await host.admin.recordMeterDay(staff, { at: '2032-04-02T00:05:00.000Z' })).toMatchObject({ taken: true });
10389
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => [r.day, r.scopes.total])).toEqual([
10390
+ ['2032-04-01', 1],
10391
+ ['2032-04-02', 2],
10392
+ ]);
10393
+ });
10394
+ it('records a failed attempt with its run and writes nothing of the day; a snapshot later that day clears the run', async () => {
10395
+ const t = await newTenant();
10396
+ const day = '2032-05-01';
10397
+ expect(await host.admin.recordMeterDay(staff, { at: `${day}T00:05:00.000Z`, error: 'directory busy' })).toEqual({
10398
+ day,
10399
+ taken: false,
10400
+ tenants: 0,
10401
+ });
10402
+ // A failure that names no run starts one of its own.
10403
+ expect(await host.admin.listMeterDayTakes(staff, { since: day, until: day })).toEqual([
10404
+ {
10405
+ day,
10406
+ attemptedAt: `${day}T00:05:00.000Z`,
10407
+ takenAt: null,
10408
+ tenants: null,
10409
+ error: 'directory busy',
10410
+ failingSince: `${day}T00:05:00.000Z`,
10411
+ failingCount: 1,
10412
+ },
10413
+ ]);
10414
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toEqual([]);
10415
+ // A failure that extends a run keeps its start and length.
10416
+ await host.admin.recordMeterDay(staff, {
10417
+ at: `${day}T00:20:00.000Z`,
10418
+ error: 'directory busy',
10419
+ failingSince: '2032-04-30T23:50:00.000Z',
10420
+ failingCount: 4,
10421
+ });
10422
+ expect((await host.admin.listMeterDayTakes(staff, { since: day, until: day }))[0]).toMatchObject({
10423
+ attemptedAt: `${day}T00:20:00.000Z`,
10424
+ failingSince: '2032-04-30T23:50:00.000Z',
10425
+ failingCount: 4,
10426
+ });
10427
+ await expect(host.admin.recordMeterDay(staff, { at: `${day}T00:25:00.000Z`, error: 'directory busy', failingCount: 0 })).rejects.toThrow(/failingCount/);
10428
+ // A snapshot taken later the same day stands, and ends the run.
10429
+ await host.admin.recordMeterDay(staff, { at: `${day}T00:35:00.000Z` });
10430
+ expect((await host.admin.listMeterDayTakes(staff, { since: day, until: day }))[0]).toEqual({
10431
+ day,
10432
+ attemptedAt: `${day}T00:35:00.000Z`,
10433
+ takenAt: `${day}T00:35:00.000Z`,
10434
+ tenants: expect.any(Number),
10435
+ error: null,
10436
+ failingSince: null,
10437
+ failingCount: null,
10438
+ });
10439
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toHaveLength(1);
10440
+ });
10441
+ it('filters by tenant and an inclusive window of days, and bounds the page', async () => {
10442
+ const t = await newTenant();
10443
+ for (const day of ['2032-06-01', '2032-06-02', '2032-06-03']) {
10444
+ await host.admin.recordMeterDay(staff, { at: `${day}T01:00:00.000Z` });
10445
+ }
10446
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => r.day)).toEqual(['2032-06-01', '2032-06-02', '2032-06-03']);
10447
+ expect((await host.admin.listMeterDays(staff, { tenantId: t, since: '2032-06-02' })).map((r) => r.day)).toEqual(['2032-06-02', '2032-06-03']);
10448
+ expect((await host.admin.listMeterDays(staff, { tenantId: t, until: '2032-06-02' })).map((r) => r.day)).toEqual(['2032-06-01', '2032-06-02']);
10449
+ expect(await host.admin.listMeterDays(staff, { tenantId: t, limit: 2 })).toHaveLength(2);
10450
+ expect((await host.admin.listMeterDayTakes(staff, { since: '2032-06-02', until: '2032-06-03' })).map((r) => r.day)).toEqual([
10451
+ '2032-06-02',
10452
+ '2032-06-03',
10453
+ ]);
10454
+ await expect(host.admin.listMeterDays(staff, { limit: 0 })).rejects.toThrow(/limit/);
10455
+ await expect(host.admin.listMeterDays(staff, { since: '2032-13-01' })).rejects.toThrow(/UTC day/);
10456
+ await expect(host.admin.listMeterDayTakes(staff, { until: '2032-02-30' })).rejects.toThrow(/UTC day/);
10457
+ });
10458
+ it('prunes snapshots older than an explicit horizon, at most the limit per call; a null horizon prunes nothing', async () => {
10459
+ // Two tenants, so every day holds at least two rows and a prune of one leaves the day in part.
10460
+ const t = await newTenant();
10461
+ await newTenant();
10462
+ const old = [monthsAgo(16), monthsAgo(15), monthsAgo(14)];
10463
+ const kept = [monthsAgo(2)];
10464
+ for (const day of [...old, ...kept])
10465
+ await host.admin.recordMeterDay(staff, { at: `${day}T12:00:00.000Z` });
10466
+ // The default keeps everything: nothing goes, however old.
10467
+ expect(await host.admin.pruneMeterDays(staff, 500, null)).toBe(0);
10468
+ expect(await host.admin.listMeterDays(staff, { tenantId: t })).toHaveLength(4);
10469
+ // Its positive twin: thirteen months named, the older days go, at most the limit per call.
10470
+ expect(await host.admin.pruneMeterDays(staff, 1, 13)).toBe(1);
10471
+ // The oldest day still holds a row, so it is still marked taken and cannot be taken again.
10472
+ expect(await host.admin.listMeterDayTakes(staff, { since: old[0], until: old[0] })).toHaveLength(1);
10473
+ while ((await host.admin.pruneMeterDays(staff, 500, 13)) > 0)
10474
+ ;
10475
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => r.day)).toEqual(kept);
10476
+ // The record of a pruned day goes with it; a kept day's stays.
10477
+ const recorded = (await host.admin.listMeterDayTakes(staff, { since: old[0], until: kept[0] })).map((r) => r.day);
10478
+ expect(recorded).toEqual(kept);
10479
+ await expect(host.admin.pruneMeterDays(staff, 0, 13)).rejects.toThrow(/limit/);
10480
+ await expect(host.admin.pruneMeterDays(staff, 10, 0)).rejects.toThrow(/whole number of months/);
10481
+ });
10482
+ it('the sweep phase takes the day on its first pass into this directory, and not again until the next day', async () => {
10483
+ const t = await newTenant();
10484
+ await newScope(t, 'meter-crm');
10485
+ const pass = (now) => runPlatformSweep(host, {
10486
+ actor: staff,
10487
+ fetch: connectorTestFetch,
10488
+ sweepers: {},
10489
+ drainRetries: false,
10490
+ meterSnapshot: { now: () => now },
10491
+ });
10492
+ const first = await pass('2033-01-01T00:04:00.000Z');
10493
+ expect(first.errors.filter((e) => e.kind === 'meter-snapshot')).toEqual([]);
10494
+ expect(first.meterSnapshot).toMatchObject({ day: '2033-01-01', taken: true, failed: 0, streak: null });
10495
+ const later = await pass('2033-01-01T00:19:00.000Z');
10496
+ expect(later.meterSnapshot).toMatchObject({ day: '2033-01-01', taken: false, tenants: 0, failed: 0 });
10497
+ const next = await pass('2033-01-02T00:04:00.000Z');
10498
+ expect(next.meterSnapshot).toMatchObject({ day: '2033-01-02', taken: true });
10499
+ expect((await host.admin.listMeterDays(staff, { tenantId: t })).map((r) => [r.day, r.readAt, r.scopes.active])).toEqual([
10500
+ ['2033-01-01', '2033-01-01T00:04:00.000Z', 1],
10501
+ ['2033-01-02', '2033-01-02T00:04:00.000Z', 1],
10502
+ ]);
10503
+ });
10504
+ });
10505
+ // -- the history of vertical ownership (#2501) ---------------------------------
10506
+ describe('the history of vertical ownership (#2501)', () => {
10507
+ const newSlug = () => `owned-${ulid().toLowerCase()}`;
10508
+ const newOwner = async () => {
10509
+ const t = tenantId.parse(ulid());
10510
+ await host.admin.createTenant(staff, { id: t, slug: `owner-${t.toLowerCase()}`, name: 'Owner' });
10511
+ return t;
10512
+ };
10513
+ const createdAtOf = async (slug) => (await host.admin.listVerticals(staff)).find((v) => v.slug === slug).createdAt;
10514
+ it('opens a period with the registration and closes it with the delete; a slug registered again opens a new one', async () => {
10515
+ const [first, second] = [await newOwner(), await newOwner()];
10516
+ const slug = newSlug();
10517
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
10518
+ const since = await createdAtOf(slug);
10519
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner: first, since, until: null }]);
10520
+ // An identical registration opens nothing; a conflicting one is refused and opens nothing.
10521
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
10522
+ await expect(host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: second })).rejects.toThrow();
10523
+ expect(await host.admin.listVerticalOwners(staff, slug)).toHaveLength(1);
10524
+ // The delete closes the period, and the history outlives the registry row.
10525
+ await host.admin.deleteVertical(staff, slug);
10526
+ const [closed] = await host.admin.listVerticalOwners(staff, slug);
10527
+ expect(closed).toEqual({ slug, owner: first, since, until: expect.any(String) });
10528
+ expect(closed.until >= since).toBe(true);
10529
+ // Registered again, by another tenant: the first owner's period stays closed, a new one opens.
10530
+ await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: second });
10531
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([
10532
+ closed,
10533
+ { slug, owner: second, since: await createdAtOf(slug), until: null },
10534
+ ]);
10535
+ // K-24: the read is attributable.
10536
+ expect((await host.admin.accessLog(staff, { method: 'listVerticalOwners' })).length).toBeGreaterThan(0);
10537
+ await host.admin.deleteVertical(staff, slug);
10538
+ });
10539
+ it("a platform vertical's owner is none, and a slug never registered has no period", async () => {
10540
+ const slug = newSlug();
10541
+ await host.admin.registerVertical(staff, { slug, name: 'Platform', source: 'builtin' });
10542
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner: null, since: await createdAtOf(slug), until: null }]);
10543
+ expect(await host.admin.listVerticalOwners(staff, newSlug())).toEqual([]);
10544
+ await host.admin.deleteVertical(staff, slug);
10545
+ });
10546
+ it("a tenant reap keeps the periods it owned: the history is the invoice's, kept for good", async () => {
10547
+ const owner = await newOwner();
10548
+ const [past, standing] = [newSlug(), newSlug()];
10549
+ await host.admin.registerVertical(staff, { slug: past, name: 'Past', source: 'cli', ownerTenant: owner });
10550
+ await host.admin.deleteVertical(staff, past);
10551
+ await host.admin.registerVertical(staff, { slug: standing, name: 'Standing', source: 'cli', ownerTenant: owner });
10552
+ const before = [await host.admin.listVerticalOwners(staff, past), await host.admin.listVerticalOwners(staff, standing)];
10553
+ await host.admin.setTenantStatus(staff, owner, 'deleting');
10554
+ await host.admin.reapTenant(staff, owner);
10555
+ expect([await host.admin.listVerticalOwners(staff, past), await host.admin.listVerticalOwners(staff, standing)]).toEqual(before);
10556
+ expect(before.flat().map((p) => p.owner)).toEqual([owner, owner]);
10557
+ await host.admin.deleteVertical(staff, standing);
10558
+ });
10559
+ it('a delete refused while a scope still runs the vertical leaves the period open', async () => {
10560
+ const owner = await newOwner();
10561
+ const slug = newSlug();
10562
+ await host.admin.registerVertical(staff, { slug, name: 'Bound', source: 'cli', ownerTenant: owner });
10563
+ await host.provisionScope(staff, { tenantId: owner, scopeId: scopeId.parse(ulid()), vertical: slug });
10564
+ await expect(host.admin.deleteVertical(staff, slug)).rejects.toThrow(/still backs/);
10565
+ expect(await host.admin.listVerticalOwners(staff, slug)).toEqual([{ slug, owner, since: await createdAtOf(slug), until: null }]);
10566
+ });
10567
+ });
9398
10568
  it('leaves bare (module-less) operations ungated', async () => {
9399
10569
  // test/read-counter was registered via defineOperation, no manifest — it
9400
10570
  // must resolve regardless of entitlements.