@substrat-run/contract-tests 0.160.0 → 0.162.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.
@@ -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
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';
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';
@@ -2368,6 +2368,21 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2368
2368
  // The spine: present and flagged system, so the UI can group it apart.
2369
2369
  expect(byName.get('_substrat_outbox')?.system).toBe(true);
2370
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
+ });
2371
2386
  it('reads a bounded page of a table', async () => {
2372
2387
  const stub = await host.getScope(alice, t1, s1);
2373
2388
  await stub.invoke('test/write-marker', { v: 'page-row' });
@@ -3126,6 +3141,29 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3126
3141
  expect(await rowsOf(ops.elsewhere)).toEqual(before.elsewhere);
3127
3142
  await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
3128
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
+ });
3129
3167
  /** Whose an exemplar is, as the directory stores it — not on the issue read shape. */
3130
3168
  const exemplarOwnerOf = async (operation) => {
3131
3169
  const dump = await host.admin.exportDirectory(staff);
@@ -7440,7 +7478,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
7440
7478
  await host.admin.setVerticalListed(staff, slug, true);
7441
7479
  await host.admin.promoteVersion(staff, slug, 'prod', first);
7442
7480
  expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
7443
- });
7481
+ // Seven scopes, a snapshot and three promotions: about 3 s alone, past the 5 s default under CI load (#2576).
7482
+ }, 30_000);
7444
7483
  it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
7445
7484
  // Listing is the moment OTHER tenants start trusting this code, so the version
7446
7485
  // they would install needs a recorded human decision — the auto-admission note
@@ -9709,8 +9748,22 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9709
9748
  const at = (time) => `2026-10-24T${time}.000Z`;
9710
9749
  /** The whole fleet, one full page: every read names its reach and its limit. */
9711
9750
  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) });
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
+ };
9714
9767
  it('keeps one record per deployment, the last upload’s, and lists one vertical’s by deployment', async () => {
9715
9768
  const [a, b] = [slugOf('wire-a'), slugOf('wire-b')];
9716
9769
  await put(a, 'serving', 'v1', record({ audience: `${a}-serving` }), '10:00:00');
@@ -9825,26 +9878,25 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9825
9878
  const [owner, other] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
9826
9879
  await host.admin.createTenant(staff, { id: owner, slug: `wire-own-${owner.slice(-6).toLowerCase()}`, name: 'Wire Owner' });
9827
9880
  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()) {
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()) {
9833
9886
  await put(slug, 'serving', null, record(), `16:00:0${i}`);
9834
9887
  await put(slug, 'archive', 'v1', record(), `16:00:1${i}`);
9835
9888
  }
9836
9889
  const refs = async (reach, verticalSlug) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, ...(verticalSlug ? { verticalSlug } : {}) }))
9837
9890
  .map((r) => r.deploymentRef)
9838
- .filter((ref) => [mine, theirs, platform, unregistered].some((slug) => ref.startsWith(`${slug}-`)));
9891
+ .filter((ref) => [mine, theirs, platform].some((slug) => ref.startsWith(`${slug}-`)));
9839
9892
  expect(await refs({ tenantId: owner })).toEqual([`${mine}-archive`, `${mine}-serving`]);
9840
9893
  expect(await refs({ tenantId: other })).toEqual([`${theirs}-archive`, `${theirs}-serving`]);
9841
9894
  // Naming another tenant's vertical inside one tenant's reach reaches nothing, not that vertical.
9842
9895
  expect(await refs({ tenantId: owner }, theirs)).toEqual([]);
9843
9896
  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.
9897
+ // A tenant that owns nothing reaches nothing; a vertical no tenant owns is reached by the fleet alone.
9846
9898
  expect(await refs({ tenantId: tenantId.parse(ulid()) })).toEqual([]);
9847
- for (const slug of [mine, theirs, platform, unregistered]) {
9899
+ for (const slug of [mine, theirs, platform]) {
9848
9900
  expect(await refs({ fleet: true }, slug)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9849
9901
  }
9850
9902
  // A page inside the reach pages over the tenant's records only.
@@ -9891,7 +9943,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9891
9943
  const owner = tenantId.parse(ulid());
9892
9944
  await host.admin.createTenant(staff, { id: owner, slug: `wire-log-${owner.slice(-6).toLowerCase()}`, name: 'Wire Log' });
9893
9945
  const mine = slugOf('wire-logged');
9894
- await host.admin.registerVertical(staff, { slug: mine, name: 'Logged', source: 'cli', ownerTenant: owner });
9946
+ await register(mine, owner);
9895
9947
  await put(mine, 'serving', null, record(), '17:00:00');
9896
9948
  // The reach given last, as a caller may spell it: the log still names it first.
9897
9949
  await host.admin.listWireAuth(staff, { limit: 5, verticalSlug: mine, reach: { tenantId: owner } });
@@ -9919,30 +9971,119 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
9919
9971
  await host.admin.createTenant(staff, { id: second, slug: `wire-snd-${second.slice(-6).toLowerCase()}`, name: 'Wire Second' });
9920
9972
  const slug = slugOf('wire-owned');
9921
9973
  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.
9974
+ // The owner reads what was recorded while it held the slug.
9975
+ await register(slug, first);
9923
9976
  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
9977
  expect(await refsOf(first)).toEqual([`${slug}-archive`]);
9927
9978
  await put(slug, 'serving', 'v1', record(), '18:00:01');
9928
9979
  expect(await refsOf(first)).toEqual([`${slug}-archive`, `${slug}-serving`]);
9929
9980
  // An owner changes by deleting the vertical, which takes its records, and registering it again:
9930
9981
  // 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 });
9982
+ await forget(slug);
9983
+ await register(slug, second);
9933
9984
  expect(await refsOf(first)).toEqual([]);
9934
9985
  expect(await refsOf(second)).toEqual([]);
9935
9986
  await put(slug, 'serving', 'v2', record(), '18:00:02');
9936
9987
  expect(await refsOf(first)).toEqual([]);
9937
9988
  expect(await refsOf(second)).toEqual([`${slug}-serving`]);
9938
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
+ });
9939
10080
  it('forgets a vertical’s records with the vertical', async () => {
9940
10081
  const a = slugOf('wire-del');
9941
- await host.admin.registerVertical(staff, { slug: a, name: 'Wire record', source: 'cli' });
10082
+ await register(a);
9942
10083
  await put(a, 'serving', null, record(), '13:00:00');
9943
10084
  const kept = slugOf('wire-kept');
9944
10085
  await put(kept, 'serving', null, record(), '13:00:01');
9945
- await host.admin.deleteVertical(staff, a);
10086
+ await forget(a);
9946
10087
  expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
9947
10088
  // Another vertical's record is not touched.
9948
10089
  expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: kept })).toHaveLength(1);
@@ -10638,6 +10779,16 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
10638
10779
  name: 'Impostor Co',
10639
10780
  }), 'conflict', /already taken/);
10640
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
+ });
10641
10792
  it('enumerates the scopes under a tenant, and filters by status (§4.5)', async () => {
10642
10793
  const all = await host.admin.listScopes(staff, { tenantId: t5 });
10643
10794
  expect(all.length).toBeGreaterThanOrEqual(2);