@substrat-run/contract-tests 0.155.0 → 0.156.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.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/membership-executor-suite.js +1 -1
- package/dist/membership-executor-suite.js.map +1 -1
- package/dist/reserved-role-suite.d.ts +1 -1
- package/dist/reserved-role-suite.d.ts.map +1 -1
- package/dist/reserved-role-suite.js +13 -2
- package/dist/reserved-role-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts +4 -0
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +216 -0
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/switch-audit-fault.d.ts +5 -0
- package/dist/switch-audit-fault.d.ts.map +1 -1
- package/dist/switch-audit-fault.js +10 -0
- package/dist/switch-audit-fault.js.map +1 -1
- package/package.json +3 -3
package/dist/scope-host-suite.js
CHANGED
|
@@ -6,6 +6,120 @@ import { billedMod, contractTestBareOps, contractTestInitialModules, gateModMani
|
|
|
6
6
|
import { testOperations } from './test-operations.js';
|
|
7
7
|
import { derivedDigestOf, heldMigrations } from './held-migrations.js';
|
|
8
8
|
import { fixtureVersion } from './fixture-version.js';
|
|
9
|
+
/** The adapter fixture installs a directory trigger, so failures exercise the real audit insert. */
|
|
10
|
+
export function scopeAdminAuditTransactionSuite(adapterName, makeFixture) {
|
|
11
|
+
describe(`HostAdmin directory audit transaction: ${adapterName}`, () => {
|
|
12
|
+
it.each(['setScopeExpiresAt', 'bindScopeVersion', 'markScopeProvisioned', 'keepAlivePendingScopeCopy',
|
|
13
|
+
'setScopeServingRef'])('%s rolls back on audit failure and writes one row on retry', async (operation) => {
|
|
14
|
+
const fixture = await makeFixture();
|
|
15
|
+
const { host } = fixture;
|
|
16
|
+
const staff = platformActorId.parse(ulid());
|
|
17
|
+
const tenant = tenantId.parse(ulid());
|
|
18
|
+
const scope = scopeId.parse(ulid());
|
|
19
|
+
const slug = `audit-${ulid().toLowerCase()}`;
|
|
20
|
+
const version = ulid();
|
|
21
|
+
let lift;
|
|
22
|
+
try {
|
|
23
|
+
await host.admin.createTenant(staff, { id: tenant, slug, name: 'Audit transaction test' });
|
|
24
|
+
await host.admin.registerVertical(staff, { slug, name: 'Audit test', source: 'builtin' });
|
|
25
|
+
await host.admin.publishVersion(staff, { id: version, verticalSlug: slug,
|
|
26
|
+
version: `1.0.${version.toLowerCase()}`, manifestDigest: 'm', permissionDigest: 'p',
|
|
27
|
+
migrationDigest: 'g', deploymentRef: null });
|
|
28
|
+
await host.admin.admitVersion(staff, version);
|
|
29
|
+
const oldExpiry = new Date(Date.now() + 60_000).toISOString();
|
|
30
|
+
await host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: slug,
|
|
31
|
+
...(operation === 'keepAlivePendingScopeCopy' ? { kind: 'preview', expiresAt: oldExpiry } : {}) });
|
|
32
|
+
if (operation !== 'keepAlivePendingScopeCopy')
|
|
33
|
+
await host.admin.activateScope(staff, tenant, scope);
|
|
34
|
+
const nextExpiry = new Date(Date.now() + 3_600_000).toISOString();
|
|
35
|
+
const servingRef = `version-${ulid().toLowerCase()}`;
|
|
36
|
+
const move = ulid();
|
|
37
|
+
if (operation === 'keepAlivePendingScopeCopy') {
|
|
38
|
+
await host.admin.recordScopeScriptCopy(staff, tenant, scope, `version-${ulid().toLowerCase()}`, move, { role: 'destination', leaseMs: 60_000 });
|
|
39
|
+
}
|
|
40
|
+
const action = operation === 'keepAlivePendingScopeCopy' ? 'setScopeExpiresAt' : operation;
|
|
41
|
+
const call = () => {
|
|
42
|
+
switch (operation) {
|
|
43
|
+
case 'setScopeExpiresAt': return host.admin.setScopeExpiresAt(staff, tenant, scope, nextExpiry);
|
|
44
|
+
case 'bindScopeVersion': return host.admin.bindScopeVersion(staff, tenant, scope, version);
|
|
45
|
+
case 'markScopeProvisioned': return host.admin.markScopeProvisioned(staff, tenant, scope, version);
|
|
46
|
+
case 'keepAlivePendingScopeCopy':
|
|
47
|
+
return host.admin.keepAlivePendingScopeCopy(staff, tenant, scope, move, 'provisioning', oldExpiry, nextExpiry);
|
|
48
|
+
case 'setScopeServingRef': return host.admin.setScopeServingRef(staff, tenant, scope, servingRef);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const state = async () => {
|
|
52
|
+
const row = (await host.admin.getScopeRecord(staff, tenant, scope));
|
|
53
|
+
return [row.expiresAt, row.verticalVersionId, row.provisionedVersionId, row.servingRef];
|
|
54
|
+
};
|
|
55
|
+
const before = await state();
|
|
56
|
+
const rows = () => host.admin.auditLog(staff, { scopeId: scope, action });
|
|
57
|
+
expect(await rows()).toEqual([]);
|
|
58
|
+
lift = await fixture.refuseAdminAction(scope, action);
|
|
59
|
+
await expect(call()).rejects.toThrow();
|
|
60
|
+
expect(await state()).toEqual(before);
|
|
61
|
+
expect(await rows()).toEqual([]);
|
|
62
|
+
await lift();
|
|
63
|
+
lift = undefined;
|
|
64
|
+
expect(await call()).not.toBe(false);
|
|
65
|
+
const after = await state();
|
|
66
|
+
const column = operation === 'bindScopeVersion' ? 1 : operation === 'markScopeProvisioned' ? 2
|
|
67
|
+
: operation === 'setScopeServingRef' ? 3 : 0;
|
|
68
|
+
expect(after[column]).toBe(operation === 'bindScopeVersion' || operation === 'markScopeProvisioned'
|
|
69
|
+
? version : operation === 'setScopeServingRef' ? servingRef : nextExpiry);
|
|
70
|
+
expect(await rows()).toHaveLength(1);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (lift)
|
|
74
|
+
await lift();
|
|
75
|
+
await fixture.cleanup();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
it('private promotion rolls back its scope bind when the audit insert fails', async () => {
|
|
79
|
+
const fixture = await makeFixture();
|
|
80
|
+
const { host } = fixture;
|
|
81
|
+
const staff = platformActorId.parse(ulid());
|
|
82
|
+
const tenant = tenantId.parse(ulid());
|
|
83
|
+
const scope = scopeId.parse(ulid());
|
|
84
|
+
const slug = `tenant-a/audit-${ulid().toLowerCase()}`;
|
|
85
|
+
let lift;
|
|
86
|
+
try {
|
|
87
|
+
await host.admin.createTenant(staff, { id: tenant, slug: `audit-${ulid().toLowerCase()}`, name: 'Audit promotion test' });
|
|
88
|
+
await host.admin.registerVertical(staff, { slug, name: 'Audit promotion test', source: 'cli', ownerTenant: tenant });
|
|
89
|
+
const publish = async () => {
|
|
90
|
+
const id = ulid();
|
|
91
|
+
await host.admin.publishVersion(staff, { id, verticalSlug: slug, version: `1.0.${id.toLowerCase()}`,
|
|
92
|
+
manifestDigest: 'm', permissionDigest: 'p', ...(await heldMigrations('g')), deploymentRef: null });
|
|
93
|
+
return id;
|
|
94
|
+
};
|
|
95
|
+
const first = await publish();
|
|
96
|
+
const second = await publish();
|
|
97
|
+
const third = await publish();
|
|
98
|
+
await host.provisionScope(staff, { tenantId: tenant, scopeId: scope, vertical: slug });
|
|
99
|
+
await host.admin.activateScope(staff, tenant, scope);
|
|
100
|
+
await host.admin.bindScopeVersion(staff, tenant, scope, first);
|
|
101
|
+
await host.admin.promoteVersion(staff, slug, 'prod', first);
|
|
102
|
+
const rows = () => host.admin.auditLog(staff, { scopeId: scope, action: 'bindScopeVersion' });
|
|
103
|
+
expect(await rows()).toHaveLength(1);
|
|
104
|
+
lift = await fixture.refuseAdminAction(scope, 'bindScopeVersion');
|
|
105
|
+
await expect(host.admin.promoteVersion(staff, slug, 'prod', second)).rejects.toThrow();
|
|
106
|
+
expect((await host.admin.getScopeRecord(staff, tenant, scope))?.verticalVersionId).toBe(first);
|
|
107
|
+
expect(await rows()).toHaveLength(1);
|
|
108
|
+
await lift();
|
|
109
|
+
lift = undefined;
|
|
110
|
+
await host.admin.promoteVersion(staff, slug, 'prod', third);
|
|
111
|
+
expect((await host.admin.getScopeRecord(staff, tenant, scope))?.verticalVersionId).toBe(third);
|
|
112
|
+
expect(await rows()).toHaveLength(2);
|
|
113
|
+
expect((await rows()).some((row) => row.after.via === 'promoteVersion')).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (lift)
|
|
117
|
+
await lift();
|
|
118
|
+
await fixture.cleanup();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
9
123
|
/**
|
|
10
124
|
* A journal read whose every row is whole. A test that planted nothing has no business meeting
|
|
11
125
|
* an `UndecodablePlatformRequest` (#1637), so one fails here, naming why, rather than being
|
|
@@ -5052,6 +5166,108 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
5052
5166
|
}
|
|
5053
5167
|
expect(beforeClaim.map((copy) => copy.moveId).sort()).toEqual([first, next].sort());
|
|
5054
5168
|
});
|
|
5169
|
+
it('a move renews its whole lease, or none of it, and never after a sweep could act (#2450)', async () => {
|
|
5170
|
+
const s = scopeId.parse(ulid());
|
|
5171
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|
|
5172
|
+
const [from, to] = [`version-${ulid().toLowerCase()}`, `version-${ulid().toLowerCase()}`];
|
|
5173
|
+
const mine = (copies) => copies.filter((c) => c.scopeId === s);
|
|
5174
|
+
const leases = async () => Object.fromEntries((await host.admin.listScopeScriptCopies(staff, { tenantId: t1, scopeId: s }))
|
|
5175
|
+
.map((c) => [`${c.moveId}:${c.scriptRef}`, c.leaseUntil]));
|
|
5176
|
+
// A live move: both ends renew to the same new end, past the one recorded.
|
|
5177
|
+
const move = ulid();
|
|
5178
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, from, move, { role: 'source', leaseMs: 60_000 });
|
|
5179
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, to, move, { role: 'destination', leaseMs: 60_000 });
|
|
5180
|
+
const before = await leases();
|
|
5181
|
+
const renewed = await host.admin.renewScopeScriptCopy(staff, t1, s, move, { leaseMs: 3_600_000 });
|
|
5182
|
+
expect(renewed).toEqual({ renewedAt: expect.any(Number), leaseUntil: expect.any(String) });
|
|
5183
|
+
const after = await leases();
|
|
5184
|
+
for (const key of Object.keys(before)) {
|
|
5185
|
+
expect(after[key]).toBe(renewed.leaseUntil);
|
|
5186
|
+
expect(Date.parse(after[key])).toBeGreaterThan(Date.parse(before[key]));
|
|
5187
|
+
}
|
|
5188
|
+
// A lease only grows: a shorter renewal answers, and moves nothing back.
|
|
5189
|
+
await host.admin.renewScopeScriptCopy(staff, t1, s, move, { leaseMs: 1_000 });
|
|
5190
|
+
expect(await leases()).toEqual(after);
|
|
5191
|
+
// So a sweep at the original lease's end claims nothing of it.
|
|
5192
|
+
const atOldEnd = new Date(Date.parse(Object.values(before)[0]) + 1).toISOString();
|
|
5193
|
+
expect(mine(await host.admin.claimExpiredScopeScriptCopies(staff, { now: atOldEnd, leaseUntil: atOldEnd, owner: 'sweep:renew', limit: 1000 }))).toEqual([]);
|
|
5194
|
+
// One end settled: the move can no longer be confirmed, so neither end renews.
|
|
5195
|
+
expect(await host.admin.settleScopeScriptCopy(staff, t1, s, to, move, 'retained')).toBe(true);
|
|
5196
|
+
const held = await leases();
|
|
5197
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, move)).toBeNull();
|
|
5198
|
+
expect(await leases()).toEqual(held);
|
|
5199
|
+
// A move of one entry renews as a move of two does, while it is live: the twin of the two
|
|
5200
|
+
// single-entry refusals below, so a host refusing every one-entry move is red here.
|
|
5201
|
+
const single = ulid();
|
|
5202
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, to, single, { role: 'destination', leaseMs: 60_000 });
|
|
5203
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, single, { leaseMs: 3_600_000 })).not.toBeNull();
|
|
5204
|
+
// A lease already run out is never revived: a sweep may be acting on it.
|
|
5205
|
+
const lapsed = ulid();
|
|
5206
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, to, lapsed, { role: 'destination', leaseMs: 1 });
|
|
5207
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
5208
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, lapsed)).toBeNull();
|
|
5209
|
+
// Nor one a sweep claimed.
|
|
5210
|
+
const claimed = ulid();
|
|
5211
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, from, claimed, { role: 'source', leaseMs: 1 });
|
|
5212
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
5213
|
+
const now = new Date().toISOString();
|
|
5214
|
+
expect(mine(await host.admin.claimExpiredScopeScriptCopies(staff, { now, leaseUntil: new Date(Date.now() + 60_000).toISOString(), owner: 'sweep:renew', limit: 1000 }))
|
|
5215
|
+
.map((c) => c.moveId)).toContain(claimed);
|
|
5216
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, claimed)).toBeNull();
|
|
5217
|
+
// A move this scope never recorded renews nothing, and neither does another tenant's key.
|
|
5218
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, ulid())).toBeNull();
|
|
5219
|
+
const other = ulid();
|
|
5220
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, from, other, { role: 'source' });
|
|
5221
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t2, s, other)).toBeNull();
|
|
5222
|
+
// Its twin: the tenant that recorded it renews it, so the refusal above is the tenant's key.
|
|
5223
|
+
expect(await host.admin.renewScopeScriptCopy(staff, t1, s, other)).not.toBeNull();
|
|
5224
|
+
});
|
|
5225
|
+
it('keeps a pending preview copy alive once, without reviving a settled move', async () => {
|
|
5226
|
+
const s = scopeId.parse(ulid());
|
|
5227
|
+
const old = new Date(Date.now() - 60_000).toISOString();
|
|
5228
|
+
const next = new Date(Date.now() + 3_600_000).toISOString();
|
|
5229
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s, kind: 'preview', expiresAt: old });
|
|
5230
|
+
const move = ulid();
|
|
5231
|
+
const ref = `version-${ulid().toLowerCase()}`;
|
|
5232
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, ref, move, { role: 'destination' });
|
|
5233
|
+
expect(await host.admin.keepAlivePendingScopeCopy(staff, t1, s, move, 'provisioning', old, next)).toBe(true);
|
|
5234
|
+
expect(await host.admin.keepAlivePendingScopeCopy(staff, t1, s, move, 'provisioning', old, next)).toBe(false);
|
|
5235
|
+
expect((await host.admin.getScopeRecord(staff, t1, s))?.expiresAt).toBe(next);
|
|
5236
|
+
expect((await host.admin.auditLog(staff, { scopeId: s, action: 'setScopeExpiresAt' })).map((e) => e.after))
|
|
5237
|
+
.toEqual([{ expiresAt: next }]);
|
|
5238
|
+
await host.admin.setScopeExpiresAt(staff, t1, s, old);
|
|
5239
|
+
await host.admin.settleScopeScriptCopy(staff, t1, s, ref, move, 'retained');
|
|
5240
|
+
expect(await host.admin.keepAlivePendingScopeCopy(staff, t1, s, move, 'provisioning', old, next)).toBe(false);
|
|
5241
|
+
expect((await host.admin.getScopeRecord(staff, t1, s))?.expiresAt).toBe(old);
|
|
5242
|
+
expect((await host.admin.auditLog(staff, { scopeId: s, action: 'setScopeExpiresAt' })).map((e) => e.after))
|
|
5243
|
+
.toEqual([{ expiresAt: next }, { expiresAt: old }]);
|
|
5244
|
+
});
|
|
5245
|
+
const lapsedMoveConditions = ['expired lease', 'claimed entry', 'mixed entries'];
|
|
5246
|
+
it.each(lapsedMoveConditions)('does not keep a preview alive with %s', async (condition) => {
|
|
5247
|
+
const s = scopeId.parse(ulid());
|
|
5248
|
+
const old = new Date(Date.now() - 60_000).toISOString();
|
|
5249
|
+
const next = new Date(Date.now() + 3_600_000).toISOString();
|
|
5250
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s, kind: 'preview', expiresAt: old });
|
|
5251
|
+
const move = ulid();
|
|
5252
|
+
const ref = `version-${ulid().toLowerCase()}`;
|
|
5253
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, ref, move, { role: 'destination', leaseMs: condition === 'mixed entries' ? 60_000 : 1 });
|
|
5254
|
+
if (condition === 'mixed entries') {
|
|
5255
|
+
await host.admin.recordScopeScriptCopy(staff, t1, s, `version-${ulid().toLowerCase()}`, move, { role: 'source' });
|
|
5256
|
+
await host.admin.settleScopeScriptCopy(staff, t1, s, ref, move, 'retained');
|
|
5257
|
+
}
|
|
5258
|
+
else {
|
|
5259
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
5260
|
+
if (condition === 'claimed entry') {
|
|
5261
|
+
expect((await host.admin.claimExpiredScopeScriptCopies(staff, {
|
|
5262
|
+
now: new Date().toISOString(), leaseUntil: new Date(Date.now() + 60_000).toISOString(),
|
|
5263
|
+
owner: 'sweep:keep-alive', limit: 1000,
|
|
5264
|
+
})).map((copy) => copy.moveId)).toContain(move);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
expect(await host.admin.keepAlivePendingScopeCopy(staff, t1, s, move, 'provisioning', old, next)).toBe(false);
|
|
5268
|
+
expect((await host.admin.getScopeRecord(staff, t1, s))?.expiresAt).toBe(old);
|
|
5269
|
+
expect(await host.admin.auditLog(staff, { scopeId: s, action: 'setScopeExpiresAt' })).toEqual([]);
|
|
5270
|
+
});
|
|
5055
5271
|
it('a backfilled copy is retained, once per script, never over a ledgered one, and never under a reap claim (#1722)', async () => {
|
|
5056
5272
|
const s = scopeId.parse(ulid());
|
|
5057
5273
|
await host.provisionScope(staff, { tenantId: t1, scopeId: s });
|