@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,
|
|
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"}
|
package/dist/scope-host-suite.js
CHANGED
|
@@ -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: '
|
|
2944
|
-
name: '
|
|
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('
|
|
2949
|
-
const [v] = await host.admin.listVersions(staff, '
|
|
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, '
|
|
2954
|
-
expect((await host.admin.listChannels(staff, '
|
|
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('
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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('
|
|
2983
|
-
await host.admin.promoteVersion(staff, '
|
|
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, '
|
|
2987
|
-
const timeline = await host.admin.listChannelHistory(staff, '
|
|
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, '
|
|
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('
|
|
3008
|
-
await host.admin.promoteVersion(staff, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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('
|
|
3028
|
-
expect((await host.admin.listVersions(staff, '
|
|
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, '
|
|
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
|
|
3707
|
-
// manifest declares. Required-alone reads as "buy the
|
|
3708
|
-
// required-vs-held shows a near-miss at a
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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());
|