@substrat-run/contract-tests 0.126.0 → 0.128.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/modules.d.ts.map +1 -1
- package/dist/modules.js +12 -0
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +27 -0
- package/dist/permission-suite.js.map +1 -1
- package/dist/schedule-suite.d.ts.map +1 -1
- package/dist/schedule-suite.js +17 -8
- package/dist/schedule-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +94 -7
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/scope-repoint-suite.d.ts.map +1 -1
- package/dist/scope-repoint-suite.js +400 -0
- package/dist/scope-repoint-suite.js.map +1 -1
- package/dist/system-switch-suite.d.ts.map +1 -1
- package/dist/system-switch-suite.js +11 -8
- package/dist/system-switch-suite.js.map +1 -1
- package/package.json +3 -3
package/dist/scope-host-suite.js
CHANGED
|
@@ -1516,6 +1516,35 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1516
1516
|
// `poison-a` failed and is backed off: an error on the row, but a retry still due.
|
|
1517
1517
|
expect(page.entries.some((d) => d.consumer === 'executor:flaky-effector')).toBe(false);
|
|
1518
1518
|
});
|
|
1519
|
+
it('replays a declared lifecycle over the outbox through the platform verb (#1744)', async () => {
|
|
1520
|
+
// `readLifecycleFlow`'s own suite pins the replay over a hand-built outbox; this
|
|
1521
|
+
// proves the wiring — real emits, on both adapters, read back through `lifecycleFlow`.
|
|
1522
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
1523
|
+
const since = new Date(Date.now() - 60_000).toISOString();
|
|
1524
|
+
await stub.invoke('test/move', { entityId: 'l1', state: 'draft' });
|
|
1525
|
+
await stub.invoke('test/move', { entityId: 'l1', state: 'live' });
|
|
1526
|
+
// No state in the payload: the declared edge says where it went.
|
|
1527
|
+
await stub.invoke('test/move', { entityId: 'l1' });
|
|
1528
|
+
const flow = await host.admin.lifecycleFlow(staff, t1, s1, {
|
|
1529
|
+
entityType: 'test-lifecycle',
|
|
1530
|
+
lifecycle: {
|
|
1531
|
+
field: 'state',
|
|
1532
|
+
initial: 'draft',
|
|
1533
|
+
states: {
|
|
1534
|
+
draft: { on: { 'test/move': 'live' } },
|
|
1535
|
+
live: { on: { 'test/move': 'done' } },
|
|
1536
|
+
done: { terminal: true },
|
|
1537
|
+
},
|
|
1538
|
+
},
|
|
1539
|
+
since,
|
|
1540
|
+
until: new Date(Date.now() + 60_000).toISOString(),
|
|
1541
|
+
});
|
|
1542
|
+
expect(flow.edges.find((e) => e.from === 'draft' && e.to === 'live').count).toBe(1);
|
|
1543
|
+
expect(flow.edges.find((e) => e.from === 'live' && e.to === 'done').count).toBe(1);
|
|
1544
|
+
expect(flow.states.find((s) => s.state === 'done').current).toBe(1);
|
|
1545
|
+
expect(flow.observation).toMatchObject({ entities: 1, events: 3, inferred: 1, complete: true });
|
|
1546
|
+
expect(flow.totals.finished).toBe(1);
|
|
1547
|
+
});
|
|
1519
1548
|
// -- scope data introspection: the §5.4 admin-query RPC --------------------
|
|
1520
1549
|
//
|
|
1521
1550
|
// A read-only window into a scope's OWN database (the console/dashboard Data
|
|
@@ -2166,10 +2195,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2166
2195
|
const dump = await host.admin.exportScope(staff, t1, s1);
|
|
2167
2196
|
// Doctor the dump into one captured before the `operation` column existed:
|
|
2168
2197
|
// the DDL loses the column and every row loses that cell — exactly what a
|
|
2169
|
-
// dump exported by an older platform carries. The
|
|
2170
|
-
//
|
|
2171
|
-
//
|
|
2172
|
-
//
|
|
2198
|
+
// dump exported by an older platform carries. The restore builds the outbox
|
|
2199
|
+
// from the kernel's DDL, not the dump's (#1883), and inserts the rows by
|
|
2200
|
+
// column name, so the missing cell reads NULL and the emit below still has
|
|
2201
|
+
// its column.
|
|
2173
2202
|
const doctored = dump.tables.map((t) => {
|
|
2174
2203
|
if (t.name !== '_substrat_outbox')
|
|
2175
2204
|
return t;
|
|
@@ -2289,6 +2318,14 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2289
2318
|
{ name: 'MARKER', ddl: 'CREATE TABLE MARKER (id TEXT)', columns: ['id'], rows: [] },
|
|
2290
2319
|
])).rejects.toThrow(/listed twice/);
|
|
2291
2320
|
});
|
|
2321
|
+
// A column listed twice is the same shape one level down: the row insert names it twice,
|
|
2322
|
+
// and which of the two cells lands is SQLite's choice, not the dump's. Case-folded, as
|
|
2323
|
+
// SQLite resolves column names that way too (#1883 review).
|
|
2324
|
+
it('refuses a column listed twice in one table, including differing only in case', async () => {
|
|
2325
|
+
await expect(importForged([
|
|
2326
|
+
{ name: 'marker', ddl: 'CREATE TABLE marker (id TEXT, note TEXT)', columns: ['id', 'note', 'ID'], rows: [] },
|
|
2327
|
+
])).rejects.toThrow(/column "ID" in table "marker" is listed twice/);
|
|
2328
|
+
});
|
|
2292
2329
|
// Non-vacuous: proves the checks do not simply reject everything, and that a
|
|
2293
2330
|
// `;` inside a string literal is not read as a statement boundary — a DEFAULT
|
|
2294
2331
|
// of `'a;b'` is legal SQL, and a `split(';')` would refuse it.
|
|
@@ -5316,9 +5353,12 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
5316
5353
|
await move(folder('f1'), folder('fr'), folder('f3'));
|
|
5317
5354
|
});
|
|
5318
5355
|
it('terminates on a graph that is already cyclic', async () => {
|
|
5319
|
-
// `link`
|
|
5320
|
-
|
|
5321
|
-
await
|
|
5356
|
+
// `link` refuses a cycle too now (#1875), so the only way one can exist is a
|
|
5357
|
+
// restored dump — c1 ⇄ c2, planted the same way an undeclared or expired edge is.
|
|
5358
|
+
await plant([
|
|
5359
|
+
{ subject: 'folder:c1', object: 'folder:c2' },
|
|
5360
|
+
{ subject: 'folder:c2', object: 'folder:c1' },
|
|
5361
|
+
]);
|
|
5322
5362
|
await link(folder('cx'), folder('fr'));
|
|
5323
5363
|
await move(folder('cx'), folder('fr'), folder('c1'));
|
|
5324
5364
|
expect(await edges(folder('cx'))).toEqual([
|
|
@@ -5329,6 +5369,53 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
5329
5369
|
await expectRefusal(move(folder('c1'), folder('c2'), folder('cx')), 'validation_failed', /its own ancestor/);
|
|
5330
5370
|
});
|
|
5331
5371
|
});
|
|
5372
|
+
/**
|
|
5373
|
+
* #1875: `ctx.link` refuses a cycle the same way `ctx.relink` does, via the same shared
|
|
5374
|
+
* walk (`entity-edges.ts`'s `assertNoCycle`) — a behaviour change to a shipped verb, since
|
|
5375
|
+
* a module that already wrote one (by accident) now fails where it used to succeed.
|
|
5376
|
+
*/
|
|
5377
|
+
describe("link refuses a cycle, sharing relink's walk (#1875)", () => {
|
|
5378
|
+
// lr ← l1 ← l2, and a sibling subtree lr ← l3.
|
|
5379
|
+
beforeAll(async () => {
|
|
5380
|
+
await link(folder('l1'), folder('lr'));
|
|
5381
|
+
await link(folder('l2'), folder('l1'));
|
|
5382
|
+
await link(folder('l3'), folder('lr'));
|
|
5383
|
+
});
|
|
5384
|
+
it('refuses linking a parent that is the child itself', async () => {
|
|
5385
|
+
await expectRefusal(link(folder('l1'), folder('l1')), 'validation_failed', /its own ancestor/);
|
|
5386
|
+
expect(await edges(folder('l1'))).toEqual([{ object: 'folder:lr', revoked: false }]);
|
|
5387
|
+
});
|
|
5388
|
+
it('refuses linking into its own descendant', async () => {
|
|
5389
|
+
// lr is an ancestor of l2 (lr ← l1 ← l2); linking lr under l2 would make lr its own
|
|
5390
|
+
// descendant.
|
|
5391
|
+
await expectRefusal(link(folder('lr'), folder('l2')), 'validation_failed', /its own ancestor/);
|
|
5392
|
+
expect(await edges(folder('lr'))).toEqual([]);
|
|
5393
|
+
});
|
|
5394
|
+
it('the twin: a link into a sibling subtree still succeeds', async () => {
|
|
5395
|
+
await link(folder('l2'), folder('l3'));
|
|
5396
|
+
expect(await edges(folder('l2'))).toEqual([
|
|
5397
|
+
{ object: 'folder:l1', revoked: false },
|
|
5398
|
+
{ object: 'folder:l3', revoked: false },
|
|
5399
|
+
]);
|
|
5400
|
+
});
|
|
5401
|
+
it('an already-cyclic graph (planted via export/restore) terminates instead of looping', async () => {
|
|
5402
|
+
// `link` refuses a cycle now, so the only way one exists is a restored dump.
|
|
5403
|
+
await plant([
|
|
5404
|
+
{ subject: 'folder:lc1', object: 'folder:lc2' },
|
|
5405
|
+
{ subject: 'folder:lc2', object: 'folder:lc1' },
|
|
5406
|
+
]);
|
|
5407
|
+
await link(folder('lc3'), folder('lr'));
|
|
5408
|
+
// lc1's own chain (lc1 → lc2 → lc1 → …) never reaches lc3, so this still succeeds —
|
|
5409
|
+
// the point is that the walk finishes at all instead of hanging on the cycle.
|
|
5410
|
+
await link(folder('lc3'), folder('lc1'));
|
|
5411
|
+
expect(await edges(folder('lc3'))).toEqual([
|
|
5412
|
+
{ object: 'folder:lc1', revoked: false },
|
|
5413
|
+
{ object: 'folder:lr', revoked: false },
|
|
5414
|
+
]);
|
|
5415
|
+
// A member of the cycle still refuses becoming its own ancestor.
|
|
5416
|
+
await expectRefusal(link(folder('lc1'), folder('lc1')), 'validation_failed', /its own ancestor/);
|
|
5417
|
+
});
|
|
5418
|
+
});
|
|
5332
5419
|
it('a tombstone survives export, restore and snapshot — never resurrected', async () => {
|
|
5333
5420
|
await link(item('rl10'), box('rb1'));
|
|
5334
5421
|
await move(item('rl10'), box('rb1'), box('rb2'));
|