@substrat-run/contract-tests 0.106.0 → 0.108.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 +1 @@
1
- {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAW9B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA2BD,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EAC5C,IAAI,GAAE,qBAA0B,GAC/B,IAAI,CA63JN"}
1
+ {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAW9B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA2BD,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EAC5C,IAAI,GAAE,qBAA0B,GAC/B,IAAI,CAk7JN"}
@@ -362,6 +362,51 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
362
362
  const journal2 = await again.invoke('testmod/read-journal');
363
363
  expect(journal2.filter((r) => r.module_id === '@test/mod')).toHaveLength(1);
364
364
  });
365
+ it('answers one record’s history through the platform (#1235)', async () => {
366
+ // `readHistory` has been the sanctioned read since #800 and, until this
367
+ // verb, nothing above the scope could call it — the whole point of #1235.
368
+ const stub = await host.getScope(alice, t1, s1);
369
+ await stub.invoke('test/emit-event');
370
+ await stub.invoke('test/emit-event', { subject: ulid(), secret: 'shh' });
371
+ const page = await host.admin.entityHistory(staff, t1, s1, {
372
+ entityType: 'test-thing',
373
+ entityId: 'x1',
374
+ });
375
+ expect(page.entries.length).toBeGreaterThanOrEqual(2);
376
+ expect(page.entries.every((e) => e.type === 'test.happened')).toBe(true);
377
+ // The fields a timeline exists to show, decoded rather than hand-parsed: the
378
+ // payload, the operation that emitted it, and the PII class that says whether
379
+ // the payload is personal at all.
380
+ const withSecret = page.entries.find((e) => e.payload?.secret === 'shh');
381
+ expect(withSecret).toBeDefined();
382
+ expect(withSecret.operation).toBe('test/emit-event');
383
+ expect(withSecret.piiClass).toBe('pseudonymous');
384
+ expect(withSecret.subjectId).not.toBeNull();
385
+ // Nobody was impersonating — a fact, not a gap, and the contract keeps the
386
+ // two apart.
387
+ expect(withSecret.impersonation).toBeNull();
388
+ // Another entity's history is a different story, never this one's.
389
+ const other = await host.admin.entityHistory(staff, t1, s1, {
390
+ entityType: 'test-thing',
391
+ entityId: 'no-such-record',
392
+ });
393
+ expect(other.entries).toEqual([]);
394
+ // Cursor-paged like every other list read: the page carries its own continuation.
395
+ const first = await host.admin.entityHistory(staff, t1, s1, {
396
+ entityType: 'test-thing',
397
+ entityId: 'x1',
398
+ limit: 1,
399
+ });
400
+ expect(first.entries).toHaveLength(1);
401
+ expect(first.nextCursor).toBe(first.entries[0].id);
402
+ const next = await host.admin.entityHistory(staff, t1, s1, {
403
+ entityType: 'test-thing',
404
+ entityId: 'x1',
405
+ limit: 1,
406
+ cursor: first.nextCursor,
407
+ });
408
+ expect(next.entries[0].id).not.toBe(first.entries[0].id);
409
+ });
365
410
  it('answers WHEN each migration ran, newest first (#1236)', async () => {
366
411
  // `applied_at` has been written since this table shipped and read by
367
412
  // nothing — every reader wanted the frontier. This is the read that makes
@@ -2940,30 +2985,30 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2940
2985
  };
2941
2986
  it('a private vertical version lands ADMITTED on push, noted as auto-admission', async () => {
2942
2987
  await host.admin.registerVertical(staff, {
2943
- slug: 'egeryds/crm',
2944
- name: 'Egeryds CRM',
2988
+ slug: 'tenant-a/crm',
2989
+ name: 'Tenant A CRM',
2945
2990
  source: 'cli',
2946
2991
  ownerTenant: t2,
2947
2992
  });
2948
- const vid = await publishPrivate('egeryds/crm', '0.1.0');
2949
- const [v] = await host.admin.listVersions(staff, 'egeryds/crm');
2993
+ const vid = await publishPrivate('tenant-a/crm', '0.1.0');
2994
+ const [v] = await host.admin.listVersions(staff, 'tenant-a/crm');
2950
2995
  expect(v?.admission).toBe('admitted');
2951
2996
  expect(v?.admissionNote).toBe(AUTO_ADMISSION_NOTE);
2952
2997
  // Promotable immediately — for a private vertical, push + promote IS the deploy.
2953
- await host.admin.promoteVersion(staff, 'egeryds/crm', 'prod', vid);
2954
- expect((await host.admin.listChannels(staff, 'egeryds/crm')).find((c) => c.channel === 'prod')?.versionId).toBe(vid);
2998
+ await host.admin.promoteVersion(staff, 'tenant-a/crm', 'prod', vid);
2999
+ expect((await host.admin.listChannels(staff, 'tenant-a/crm')).find((c) => c.channel === 'prod')?.versionId).toBe(vid);
2955
3000
  });
2956
3001
  it('getVersion reads ONE version by id, and fails closed across a lineage', async () => {
2957
3002
  // The read that replaced "list every version this vertical ever pushed, then
2958
3003
  // .find() one". Correctness first: same row, same shape as the list gives.
2959
- const vid = await publishPrivate('egeryds/crm', '0.9.0');
3004
+ const vid = await publishPrivate('tenant-a/crm', '0.9.0');
2960
3005
  const one = await host.admin.getVersion(staff, vid);
2961
3006
  expect(one?.id).toBe(vid);
2962
- expect(one).toEqual((await host.admin.listVersions(staff, 'egeryds/crm')).find((v) => v.id === vid));
3007
+ expect(one).toEqual((await host.admin.listVersions(staff, 'tenant-a/crm')).find((v) => v.id === vid));
2963
3008
  // Narrowed, it keeps what the old `.find()`-inside-one-slug's-list gave for free:
2964
3009
  // a version of ANOTHER vertical is absent, not returned across the boundary. Without
2965
3010
  // this, the slug in the URL stops constraining which version a route can hand back.
2966
- expect((await host.admin.getVersion(staff, vid, 'egeryds/crm'))?.id).toBe(vid);
3011
+ expect((await host.admin.getVersion(staff, vid, 'tenant-a/crm'))?.id).toBe(vid);
2967
3012
  expect(await host.admin.getVersion(staff, vid, 'callout')).toBeUndefined();
2968
3013
  // An id nobody published is absent, not a throw — callers branch on undefined.
2969
3014
  expect(await host.admin.getVersion(staff, ulid())).toBeUndefined();
@@ -2976,15 +3021,15 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2976
3021
  expect(versions.find((v) => v.version === '9.9.9')?.admission).toBe('pending');
2977
3022
  });
2978
3023
  it('promotion appends to the channel history — the go-live timeline rollback picks from', async () => {
2979
- const first = (await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod'));
3024
+ const first = (await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod'));
2980
3025
  expect(first).toHaveLength(1);
2981
3026
  expect(first[0].fromVersionId).toBeNull(); // nothing before the first go-live
2982
- const v2 = await publishPrivate('egeryds/crm', '0.2.0');
2983
- await host.admin.promoteVersion(staff, 'egeryds/crm', 'prod', v2);
3027
+ const v2 = await publishPrivate('tenant-a/crm', '0.2.0');
3028
+ await host.admin.promoteVersion(staff, 'tenant-a/crm', 'prod', v2);
2984
3029
  // Rollback is a NEW promotion of the older version — the timeline only grows.
2985
3030
  const v1 = first[0].versionId;
2986
- await host.admin.promoteVersion(staff, 'egeryds/crm', 'prod', v1);
2987
- const timeline = await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod');
3031
+ await host.admin.promoteVersion(staff, 'tenant-a/crm', 'prod', v1);
3032
+ const timeline = await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod');
2988
3033
  expect(timeline.map((h) => h.versionId)).toEqual([v1, v2, v1]); // newest first
2989
3034
  expect(timeline[0].fromVersionId).toBe(v2);
2990
3035
  expect(timeline[1].fromVersionId).toBe(v1);
@@ -3001,33 +3046,33 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3001
3046
  const appScope = scopeId.parse(ulid());
3002
3047
  await host.provisionScope(staff, { tenantId: t2, scopeId: appScope });
3003
3048
  await host.admin.activateScope(staff, t2, appScope);
3004
- const prodNow = (await host.admin.listChannels(staff, 'egeryds/crm')).find((c) => c.channel === 'prod')
3049
+ const prodNow = (await host.admin.listChannels(staff, 'tenant-a/crm')).find((c) => c.channel === 'prod')
3005
3050
  .versionId;
3006
3051
  await host.admin.bindScopeVersion(staff, t2, appScope, prodNow);
3007
- const v3 = await publishPrivate('egeryds/crm', '0.3.0');
3008
- await host.admin.promoteVersion(staff, 'egeryds/crm', 'prod', v3);
3052
+ const v3 = await publishPrivate('tenant-a/crm', '0.3.0');
3053
+ await host.admin.promoteVersion(staff, 'tenant-a/crm', 'prod', v3);
3009
3054
  expect((await host.admin.getScopeRecord(staff, t2, appScope))?.verticalVersionId).toBe(v3);
3010
3055
  });
3011
3056
  it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
3012
3057
  // Listing is the moment OTHER tenants start trusting this code, so the version
3013
3058
  // they would install needs a recorded human decision — the auto-admission note
3014
3059
  // is exactly what marks its absence.
3015
- await expect(host.admin.setVerticalListed(staff, 'egeryds/crm', true)).rejects.toThrow(/auto-admitted.*staff admit/);
3060
+ await expect(host.admin.setVerticalListed(staff, 'tenant-a/crm', true)).rejects.toThrow(/auto-admitted.*staff admit/);
3016
3061
  // A staff admit of the already-admitted version upgrades it to a manual vouch
3017
3062
  // (clears the note, audited) — then listing passes.
3018
- const prodNow = (await host.admin.listChannels(staff, 'egeryds/crm')).find((c) => c.channel === 'prod')
3063
+ const prodNow = (await host.admin.listChannels(staff, 'tenant-a/crm')).find((c) => c.channel === 'prod')
3019
3064
  .versionId;
3020
3065
  await host.admin.admitVersion(staff, prodNow);
3021
- const upgraded = (await host.admin.listVersions(staff, 'egeryds/crm')).find((v) => v.id === prodNow);
3066
+ const upgraded = (await host.admin.listVersions(staff, 'tenant-a/crm')).find((v) => v.id === prodNow);
3022
3067
  expect(upgraded?.admission).toBe('admitted');
3023
3068
  expect(upgraded?.admissionNote).toBeNull();
3024
- await host.admin.setVerticalListed(staff, 'egeryds/crm', true);
3069
+ await host.admin.setVerticalListed(staff, 'tenant-a/crm', true);
3025
3070
  // Listed now: the next push lands PENDING — staff admission is back in the
3026
3071
  // path exactly when the audience widened.
3027
- const v4 = await publishPrivate('egeryds/crm', '0.4.0');
3028
- expect((await host.admin.listVersions(staff, 'egeryds/crm')).find((v) => v.id === v4)?.admission).toBe('pending');
3072
+ const v4 = await publishPrivate('tenant-a/crm', '0.4.0');
3073
+ expect((await host.admin.listVersions(staff, 'tenant-a/crm')).find((v) => v.id === v4)?.admission).toBe('pending');
3029
3074
  // Unlist again so later suites see the vertical private (and pushes self-admit).
3030
- await host.admin.setVerticalListed(staff, 'egeryds/crm', false);
3075
+ await host.admin.setVerticalListed(staff, 'tenant-a/crm', false);
3031
3076
  });
3032
3077
  // -- the provisioning state (K-31) ---------------------------------------
3033
3078
  it('provisions a scope as `provisioning`, not `active`', async () => {
@@ -3703,9 +3748,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3703
3748
  const stub = await host.getScope(alice, t4, s4);
3704
3749
  // Holding nothing: the denial says so, rather than leaving "held" to the imagination.
3705
3750
  await expect(stub.invoke('billed/act')).rejects.toThrow(/holds: none/);
3706
- // The Egeryds shape (#691): the tenant DOES hold keys — just not under the name the
3707
- // manifest declares. Required-alone reads as "buy the SKU" and sends you shopping;
3708
- // required-vs-held shows a near-miss at a glance, which is the actual diagnosis.
3751
+ // The slug-prefixed-key shape (#691): the tenant DOES hold keys — just not
3752
+ // under the name the manifest declares. Required-alone reads as "buy the
3753
+ // SKU" and sends you shopping; required-vs-held shows a near-miss at a
3754
+ // glance, which is the actual diagnosis.
3709
3755
  await host.admin.grantEntitlement(staff, t4, 't-0wv2mwk4j5/billed');
3710
3756
  await host.admin.grantEntitlement(staff, t4, 'workorder');
3711
3757
  const err = await stub.invoke('billed/act').then(() => null, (e) => e.message);
@@ -4102,12 +4148,12 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4102
4148
  expect(seen).toEqual(all.map((v) => v.slug));
4103
4149
  });
4104
4150
  it('flips listVersions to newest-first on order desc, cursor still exclusive', async () => {
4105
- const asc = await host.admin.listVersions(staff, 'egeryds/crm');
4151
+ const asc = await host.admin.listVersions(staff, 'tenant-a/crm');
4106
4152
  expect(asc.length).toBeGreaterThanOrEqual(4);
4107
- const desc = await host.admin.listVersions(staff, 'egeryds/crm', { order: 'desc' });
4153
+ const desc = await host.admin.listVersions(staff, 'tenant-a/crm', { order: 'desc' });
4108
4154
  expect(desc.map((v) => v.id)).toEqual([...asc.map((v) => v.id)].reverse());
4109
4155
  // Descending pages backward from the cursor, strictly before it.
4110
- const page = await host.admin.listVersions(staff, 'egeryds/crm', {
4156
+ const page = await host.admin.listVersions(staff, 'tenant-a/crm', {
4111
4157
  order: 'desc',
4112
4158
  limit: 2,
4113
4159
  cursor: desc[0].id,
@@ -4115,19 +4161,19 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4115
4161
  expect(page.map((v) => v.id)).toEqual(desc.slice(1, 3).map((v) => v.id));
4116
4162
  });
4117
4163
  it('keeps listChannelHistory newest-first by default and pages it; asc flips the walk', async () => {
4118
- const newest = await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod');
4164
+ const newest = await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod');
4119
4165
  expect(newest.length).toBeGreaterThanOrEqual(4);
4120
4166
  // Entry ids are ULIDs, so newest-first means descending ids — the shipped order.
4121
4167
  expect(newest.map((h) => h.id)).toEqual([...newest.map((h) => h.id)].sort().reverse());
4122
- const first = await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod', { limit: 2 });
4168
+ const first = await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod', { limit: 2 });
4123
4169
  expect(first.map((h) => h.id)).toEqual(newest.slice(0, 2).map((h) => h.id));
4124
- const next = await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod', {
4170
+ const next = await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod', {
4125
4171
  limit: 2,
4126
4172
  cursor: first[1].id,
4127
4173
  });
4128
4174
  expect(next.map((h) => h.id)).toEqual(newest.slice(2, 4).map((h) => h.id));
4129
4175
  // 'asc' flips to oldest-first — the whole timeline, reversed.
4130
- const oldest = await host.admin.listChannelHistory(staff, 'egeryds/crm', 'prod', {
4176
+ const oldest = await host.admin.listChannelHistory(staff, 'tenant-a/crm', 'prod', {
4131
4177
  order: 'asc',
4132
4178
  });
4133
4179
  expect(oldest.map((h) => h.id)).toEqual([...newest.map((h) => h.id)].reverse());