@substrat-run/contract-tests 0.121.0 → 0.122.1
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":"AA4BA,OAAO,EAOL,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;AA6DD,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":"AA4BA,OAAO,EAOL,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;AA6DD,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,CA0hON"}
|
package/dist/scope-host-suite.js
CHANGED
|
@@ -1127,6 +1127,42 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1127
1127
|
await expect(host.admin.markEventsDrained(staff, t2, sDrain, [])).rejects.toThrow(/unknown scope/);
|
|
1128
1128
|
await expect(host.admin.markEventsDrained(staff, t1, scopeId.parse(ulid()), [])).rejects.toThrow(/unknown scope/);
|
|
1129
1129
|
});
|
|
1130
|
+
it('stamps and reopens more events than a Durable Object binds parameters (#1776)', async () => {
|
|
1131
|
+
// A DO's SQLite refuses the 101st bound parameter (#1741), and the drain batch is 200
|
|
1132
|
+
// by default. A statement binding one `?` per event id therefore failed every default
|
|
1133
|
+
// drain of a busy scope on the hosted host, while node, which allows far more, stayed
|
|
1134
|
+
// green. Its own scope, so the counts below are this test's events and nothing else.
|
|
1135
|
+
const sBig = scopeId.parse(ulid());
|
|
1136
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: sBig, vertical: 'connector-vertical' });
|
|
1137
|
+
await host.admin.activateScope(staff, t1, sBig);
|
|
1138
|
+
const stub = await host.getScope(alice, t1, sBig);
|
|
1139
|
+
await stub.invoke('test/emit-burst', { entityId: ulid(), count: 350 });
|
|
1140
|
+
const pending = await host.admin.readUndrainedEvents(staff, t1, sBig, 1000);
|
|
1141
|
+
expect(pending).toHaveLength(350);
|
|
1142
|
+
const ids = pending.map((e) => e.id);
|
|
1143
|
+
// 150: past the limit. Then 200, the default batch, overlapping the first so the
|
|
1144
|
+
// count has to tell a stamped row from an unstamped one inside one oversized list.
|
|
1145
|
+
await expect(host.admin.markEventsDrained(staff, t1, sBig, ids.slice(0, 150))).resolves.toBe(150);
|
|
1146
|
+
// The two batches must carry different instants for the reopen below to tell them apart.
|
|
1147
|
+
const firstMarked = Date.now();
|
|
1148
|
+
while (Date.now() <= firstMarked + 1)
|
|
1149
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
1150
|
+
await expect(host.admin.markEventsDrained(staff, t1, sBig, ids.slice(100, 350))).resolves.toBe(200);
|
|
1151
|
+
await expect(host.admin.readUndrainedEvents(staff, t1, sBig, 1000)).resolves.toHaveLength(0);
|
|
1152
|
+
const stamps = (await host.admin.auditLog(staff, { tenantId: t1 }))
|
|
1153
|
+
.filter((r) => r.action === 'drainEvents' && r.scopeId === sBig)
|
|
1154
|
+
.map((r) => r.after)
|
|
1155
|
+
.sort((a, b) => (a.drainedAt < b.drainedAt ? -1 : 1));
|
|
1156
|
+
expect(stamps.map((s) => [s.drained, s.requested])).toEqual([[150, 150], [200, 250]]);
|
|
1157
|
+
// The reopen, past the limit the same way. Just after the FIRST instant it reopens exactly
|
|
1158
|
+
// the first 150, which is what shows the overlap kept its first stamp: a stamp that
|
|
1159
|
+
// re-marked rows 100–149 would have moved them out of this window.
|
|
1160
|
+
const justAfter = (at) => new Date(Date.parse(at) + 1).toISOString();
|
|
1161
|
+
await expect(host.admin.redrainEvents(staff, t1, sBig, { drainedBefore: justAfter(stamps[0].drainedAt) })).resolves.toBe(150);
|
|
1162
|
+
expect((await host.admin.readUndrainedEvents(staff, t1, sBig, 1000)).map((e) => e.id)).toEqual(ids.slice(0, 150));
|
|
1163
|
+
await expect(host.admin.redrainEvents(staff, t1, sBig, { drainedBefore: justAfter(stamps[1].drainedAt) })).resolves.toBe(200);
|
|
1164
|
+
expect((await host.admin.readUndrainedEvents(staff, t1, sBig, 1000)).map((e) => e.id)).toEqual(ids);
|
|
1165
|
+
});
|
|
1130
1166
|
it('facets the outbox, and keeps an ERASED payload out of the null bucket (#1239)', async () => {
|
|
1131
1167
|
const stub = await host.getScope(alice, t1, s1);
|
|
1132
1168
|
const subject = ulid();
|
|
@@ -4165,6 +4201,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
4165
4201
|
await expectRefusal(host.admin.promoteVersion(staff, 'callout', 'prod', ghost), 'not_found');
|
|
4166
4202
|
await expectRefusal(host.admin.bindScopeVersion(staff, t1, s1, ghost), 'not_found');
|
|
4167
4203
|
await expectRefusal(host.admin.versionManifest(staff, 'callout', ghost), 'not_found');
|
|
4204
|
+
await expectRefusal(host.admin.versionMigrations(staff, 'callout', ghost), 'not_found');
|
|
4168
4205
|
// versionManifest refuses on the PAIR — `!v || v.vertical_slug !== verticalSlug` —
|
|
4169
4206
|
// so the absent id above only exercises HALF its guard. A version that really
|
|
4170
4207
|
// exists, under a DIFFERENT vertical, must read as absent too: asking for it under
|
|
@@ -4191,6 +4228,56 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
4191
4228
|
// firing, not the version failing to exist.
|
|
4192
4229
|
await host.admin.versionManifest(staff, 'otherowner', elsewhere);
|
|
4193
4230
|
await expectRefusal(host.admin.versionManifest(staff, 'callout', elsewhere), 'not_found');
|
|
4231
|
+
// The migrations read refuses on the same pair: a version's SQL describes its schema.
|
|
4232
|
+
await host.admin.versionMigrations(staff, 'otherowner', elsewhere);
|
|
4233
|
+
await expectRefusal(host.admin.versionMigrations(staff, 'callout', elsewhere), 'not_found');
|
|
4234
|
+
});
|
|
4235
|
+
it('stores a version\'s SQL migrations apart from its manifest, and never reads absent as none (#1764)', async () => {
|
|
4236
|
+
await host.admin.registerVertical(staff, { slug: 'migrationsapart', name: 'Apart', source: 'builtin' });
|
|
4237
|
+
const base = {
|
|
4238
|
+
version: '1.0.0',
|
|
4239
|
+
entry: 'index.js',
|
|
4240
|
+
registry: { permissions: [], roles: [], entityGrants: [] },
|
|
4241
|
+
digests: { manifest: 'm', permission: 'p', migration: 'g' },
|
|
4242
|
+
};
|
|
4243
|
+
const publish = async (manifest) => {
|
|
4244
|
+
const id = ulid();
|
|
4245
|
+
await host.admin.publishVersion(staff, {
|
|
4246
|
+
id, verticalSlug: 'migrationsapart', version: `1.0.${id.toLowerCase()}`, manifestDigest: 'm',
|
|
4247
|
+
permissionDigest: 'p', migrationDigest: 'g', deploymentRef: null,
|
|
4248
|
+
manifestJson: typeof manifest === 'string' ? manifest : JSON.stringify(manifest),
|
|
4249
|
+
});
|
|
4250
|
+
return id;
|
|
4251
|
+
};
|
|
4252
|
+
const migrations = [
|
|
4253
|
+
{ moduleId: 'engine-workorder', version: '0001-init', sql: 'CREATE TABLE a (id TEXT);' },
|
|
4254
|
+
{ moduleId: 'helpdesk', version: '0001-init', sql: 'CREATE TABLE b (id TEXT);' },
|
|
4255
|
+
{ moduleId: 'helpdesk', version: '0002-more', sql: 'ALTER TABLE b ADD COLUMN c TEXT;' },
|
|
4256
|
+
];
|
|
4257
|
+
// Carried: every migration, in the order the host runs them, and a manifest without them.
|
|
4258
|
+
const carried = await publish({ ...base, migrations });
|
|
4259
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', carried)).toEqual(migrations);
|
|
4260
|
+
const manifest = JSON.parse((await host.admin.versionManifest(staff, 'migrationsapart', carried)));
|
|
4261
|
+
expect(manifest).not.toHaveProperty('migrations');
|
|
4262
|
+
expect(manifest).toEqual(base);
|
|
4263
|
+
// `[]` is a version whose modules ship none; an absent field is one that cannot say.
|
|
4264
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', await publish({ ...base, migrations: [] }))).toEqual([]);
|
|
4265
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', await publish(base))).toBeNull();
|
|
4266
|
+
// Over the push's caps, or not shaped like migrations: dropped, and read as not available.
|
|
4267
|
+
// `/deploy` refuses such a set before the upload; this is the cap on whatever else reaches
|
|
4268
|
+
// `publishVersion` (the raw `POST …/versions`, #1765), and the publish still succeeds.
|
|
4269
|
+
const overCap = await publish({
|
|
4270
|
+
...base,
|
|
4271
|
+
migrations: [{ moduleId: 'helpdesk', version: '0001', sql: 'x'.repeat(512 * 1024 + 1) }],
|
|
4272
|
+
});
|
|
4273
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', overCap)).toBeNull();
|
|
4274
|
+
expect(JSON.parse((await host.admin.versionManifest(staff, 'migrationsapart', overCap)))).not.toHaveProperty('migrations');
|
|
4275
|
+
const malformed = await publish({ ...base, migrations: 'nope' });
|
|
4276
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', malformed)).toBeNull();
|
|
4277
|
+
// A manifest that is not JSON, or none at all: stored as it came, with no SQL to show.
|
|
4278
|
+
const junk = await publish('not json');
|
|
4279
|
+
expect(await host.admin.versionManifest(staff, 'migrationsapart', junk)).toBe('not json');
|
|
4280
|
+
expect(await host.admin.versionMigrations(staff, 'migrationsapart', junk)).toBeNull();
|
|
4194
4281
|
});
|
|
4195
4282
|
it('an archived scope blocks the delete naming the reap step; a reaped tombstone never blocks', async () => {
|
|
4196
4283
|
// A deleted app leaves an `archived` row (restorable via unarchive), then a
|
|
@@ -5805,6 +5892,13 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
5805
5892
|
// An empty status list means "no status is acceptable" — it must match
|
|
5806
5893
|
// nothing, never degenerate into an unfiltered read of the whole fleet.
|
|
5807
5894
|
expect(await host.admin.listScopes(staff, { tenantId: t5, status: [] })).toEqual([]);
|
|
5895
|
+
// A list longer than a Durable Object binds parameters (#1776). Nothing bounds its
|
|
5896
|
+
// length, since a status may repeat, and a `?` per entry was refused past 100.
|
|
5897
|
+
const repeated = await host.admin.listScopes(staff, {
|
|
5898
|
+
tenantId: t5,
|
|
5899
|
+
status: [...Array(150).fill('active'), 'suspended'],
|
|
5900
|
+
});
|
|
5901
|
+
expect(repeated.map((s) => s.id)).toEqual(both.map((s) => s.id));
|
|
5808
5902
|
await host.admin.unsuspendScope(staff, t5, target.id);
|
|
5809
5903
|
});
|
|
5810
5904
|
it('lists the whole fleet across tenants when unfiltered (§4.5)', async () => {
|
|
@@ -5865,6 +5959,13 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
5865
5959
|
expect(provisions.every((r) => r.action === 'provisionScope')).toBe(true);
|
|
5866
5960
|
// Empty action list matches nothing rather than everything.
|
|
5867
5961
|
expect(await host.admin.auditLog(staff, { tenantId: t5, action: [] })).toEqual([]);
|
|
5962
|
+
// A list longer than a Durable Object binds parameters (#1776): an action may repeat,
|
|
5963
|
+
// so nothing bounds its length, and a `?` per entry was refused past 100.
|
|
5964
|
+
const repeated = await host.admin.auditLog(staff, {
|
|
5965
|
+
tenantId: t5,
|
|
5966
|
+
action: [...Array(150).fill('suspendScope'), 'unsuspendScope'],
|
|
5967
|
+
});
|
|
5968
|
+
expect(repeated.map((r) => r.id)).toEqual(lifecycle.map((r) => r.id));
|
|
5868
5969
|
});
|
|
5869
5970
|
it('orders oldest-first by default and newest-first on request (§4.5)', async () => {
|
|
5870
5971
|
const asc = await host.admin.auditLog(staff, { tenantId: t5 });
|