@substrat-run/contract-tests 0.160.0 → 0.161.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/directory-audit-suite.d.ts +6 -0
- package/dist/directory-audit-suite.d.ts.map +1 -1
- package/dist/directory-audit-suite.js +217 -2
- package/dist/directory-audit-suite.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/introspection-reads-suite.d.ts +35 -0
- package/dist/introspection-reads-suite.d.ts.map +1 -0
- package/dist/introspection-reads-suite.js +114 -0
- package/dist/introspection-reads-suite.js.map +1 -0
- package/dist/restore-role-suite.d.ts +17 -0
- package/dist/restore-role-suite.d.ts.map +1 -0
- package/dist/restore-role-suite.js +107 -0
- package/dist/restore-role-suite.js.map +1 -0
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +172 -21
- package/dist/scope-host-suite.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { errorCodeOf, eventId, platformActorId, scopeId as scopeIdSchema, tenantId, } from '@substrat-run/contracts';
|
|
3
|
+
import { scopeNotHeldRefusal, ulid } from '@substrat-run/kernel';
|
|
4
|
+
import { TEST_LIFECYCLE } from './modules.js';
|
|
5
|
+
/** A host's introspection reads, bound, for a fixture to hand the suite. */
|
|
6
|
+
export const introspectionReadsOf = (host) => ({
|
|
7
|
+
facetEventsLocal: (s, i) => host.facetEventsLocal(s, i),
|
|
8
|
+
eventEffectsLocal: (s, i) => host.eventEffectsLocal(s, i),
|
|
9
|
+
invocationEventsLocal: (s, i) => host.invocationEventsLocal(s, i),
|
|
10
|
+
deadLettersLocal: (s, i) => host.deadLettersLocal(s, i),
|
|
11
|
+
lifecycleFlowLocal: (s, i) => host.lifecycleFlowLocal(s, i),
|
|
12
|
+
operationSeriesLocal: (s, i) => host.operationSeriesLocal(s, i),
|
|
13
|
+
eventCauseLocal: (s, i) => host.eventCauseLocal(s, i),
|
|
14
|
+
entityHistoryLocal: (s, i) => host.entityHistoryLocal(s, i),
|
|
15
|
+
appliedMigrationsLocal: (s) => host.appliedMigrationsLocal(s),
|
|
16
|
+
migrationBookmarksLocal: (s) => host.migrationBookmarksLocal(s),
|
|
17
|
+
introspectScopeTables: (s) => host.introspectScopeTables(s),
|
|
18
|
+
introspectScopeTable: (s, i) => host.introspectScopeTable(s, i),
|
|
19
|
+
introspectScopeQuery: (s, i) => host.introspectScopeQuery(s, i),
|
|
20
|
+
undrainedEventsLocal: (s, n) => host.undrainedEventsLocal(s, n),
|
|
21
|
+
redrainCountLocal: (s, b) => host.redrainCountLocal(s, b),
|
|
22
|
+
});
|
|
23
|
+
/** One valid call of each read, for a suite that asks every one of them the same question. */
|
|
24
|
+
export function introspectionReadCalls(r) {
|
|
25
|
+
const window = () => {
|
|
26
|
+
const since = Math.floor(Date.now() / 1000) * 1000;
|
|
27
|
+
return { since: new Date(since - 60_000).toISOString(), until: new Date(since + 60_000).toISOString() };
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
facetEventsLocal: (s) => r.facetEventsLocal(s, { groupBy: { kind: 'type' } }),
|
|
31
|
+
eventEffectsLocal: (s) => r.eventEffectsLocal(s, { eventId: eventId.parse(ulid()) }),
|
|
32
|
+
invocationEventsLocal: (s) => r.invocationEventsLocal(s, { invocationId: ulid() }),
|
|
33
|
+
deadLettersLocal: (s) => r.deadLettersLocal(s, {}),
|
|
34
|
+
lifecycleFlowLocal: (s) => r.lifecycleFlowLocal(s, { entityType: 'test-lifecycle', lifecycle: TEST_LIFECYCLE, ...window() }),
|
|
35
|
+
operationSeriesLocal: (s) => r.operationSeriesLocal(s, {
|
|
36
|
+
moves: [{ entityType: 'test-lifecycle', operation: 'test/move' }],
|
|
37
|
+
bucketMinutes: 1,
|
|
38
|
+
...window(),
|
|
39
|
+
}),
|
|
40
|
+
eventCauseLocal: (s) => r.eventCauseLocal(s, { eventId: eventId.parse(ulid()) }),
|
|
41
|
+
entityHistoryLocal: (s) => r.entityHistoryLocal(s, { entityType: 'test-thing', entityId: 'x1' }),
|
|
42
|
+
appliedMigrationsLocal: (s) => r.appliedMigrationsLocal(s),
|
|
43
|
+
migrationBookmarksLocal: (s) => r.migrationBookmarksLocal(s),
|
|
44
|
+
introspectScopeTables: (s) => r.introspectScopeTables(s),
|
|
45
|
+
introspectScopeTable: (s) => r.introspectScopeTable(s, { table: '_substrat_outbox', limit: 10, offset: 0 }),
|
|
46
|
+
introspectScopeQuery: (s) => r.introspectScopeQuery(s, { sql: 'SELECT 1 AS one' }),
|
|
47
|
+
undrainedEventsLocal: (s) => r.undrainedEventsLocal(s, 10),
|
|
48
|
+
redrainCountLocal: (s) => r.redrainCountLocal(s, new Date().toISOString()),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The introspection reads that name only a scope id hold the scope before they read it (#2544), as
|
|
53
|
+
* the size reads do (#2541): a scope this deployment does not hold (reaped, or never provisioned
|
|
54
|
+
* here) is refused `conflict` with one sentence, never answered as an empty store, and a refusal
|
|
55
|
+
* creates no store. A held scope still reads. One case per read, on both adapters.
|
|
56
|
+
*/
|
|
57
|
+
export function introspectionReadsContractSuite(adapterName, makeFixture) {
|
|
58
|
+
describe(`introspection reads refuse a scope the deployment does not hold (#2544): ${adapterName}`, () => {
|
|
59
|
+
let fixture;
|
|
60
|
+
const staff = platformActorId.parse(ulid());
|
|
61
|
+
const t = tenantId.parse(ulid());
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
fixture = await makeFixture();
|
|
64
|
+
await fixture.host.admin.createTenant(staff, { id: t, slug: `introspect-${t.slice(-8).toLowerCase()}`, name: 'Introspection' });
|
|
65
|
+
});
|
|
66
|
+
afterAll(async () => fixture?.cleanup());
|
|
67
|
+
const newScope = async () => {
|
|
68
|
+
const s = scopeIdSchema.parse(ulid());
|
|
69
|
+
await fixture.host.provisionScope(staff, { tenantId: t, scopeId: s, vertical: 'introspect' });
|
|
70
|
+
await fixture.host.admin.activateScope(staff, t, s);
|
|
71
|
+
return s;
|
|
72
|
+
};
|
|
73
|
+
const reap = async (s) => {
|
|
74
|
+
await fixture.host.admin.archiveScope(staff, t, s);
|
|
75
|
+
await fixture.host.admin.reapScope(staff, t, s, { force: true });
|
|
76
|
+
};
|
|
77
|
+
const refusal = (p) => p.then(() => undefined, (e) => ({ code: errorCodeOf(e), message: e.message }));
|
|
78
|
+
const refusedAs = (s) => ({ code: 'conflict', message: scopeNotHeldRefusal(s) });
|
|
79
|
+
// The calls are built before the fixture is (they name the cases), so each one reaches it late.
|
|
80
|
+
const lazy = new Proxy({}, {
|
|
81
|
+
get: (_target, name) => (...args) => fixture[name](...args),
|
|
82
|
+
});
|
|
83
|
+
const reads = introspectionReadCalls(lazy);
|
|
84
|
+
describe.each(Object.entries(reads))('%s', (_name, read) => {
|
|
85
|
+
it('reads a held scope, and answers something', async () => {
|
|
86
|
+
const s = await newScope();
|
|
87
|
+
expect(await read(s)).not.toBeNull();
|
|
88
|
+
expect(await read(s)).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
it('refuses a reaped scope, in the shared words, and leaves nothing stored for it', async () => {
|
|
91
|
+
const s = await newScope();
|
|
92
|
+
await reap(s);
|
|
93
|
+
expect(await refusal(read(s))).toEqual(refusedAs(s));
|
|
94
|
+
// Asked again, it is still refused: the first refusal did not make the scope held.
|
|
95
|
+
expect(await refusal(read(s))).toEqual(refusedAs(s));
|
|
96
|
+
expect(await fixture.storeHoldsNothing(t, s)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('refuses an id nothing provisioned here, and leaves nothing stored for it', async () => {
|
|
99
|
+
const s = scopeIdSchema.parse(ulid());
|
|
100
|
+
expect(await refusal(read(s))).toEqual(refusedAs(s));
|
|
101
|
+
expect(await fixture.storeHoldsNothing(t, s)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('a scope listed as active and reaped before it is read is refused, while the held one beside it reads', async () => {
|
|
105
|
+
const [gone, kept] = [await newScope(), await newScope()];
|
|
106
|
+
const due = (await fixture.host.admin.listScopes(staff, { tenantId: t, status: 'active' })).map((x) => x.id);
|
|
107
|
+
expect(due).toEqual(expect.arrayContaining([gone, kept]));
|
|
108
|
+
await reap(gone);
|
|
109
|
+
expect(await refusal(fixture.undrainedEventsLocal(gone, 10))).toEqual(refusedAs(gone));
|
|
110
|
+
expect(await refusal(fixture.undrainedEventsLocal(kept, 10))).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=introspection-reads-suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection-reads-suite.js","sourceRoot":"","sources":["../src/introspection-reads-suite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ScopeId, type TenantId } from '@substrat-run/contracts';
|
|
2
|
+
import { type RoleState } from '@substrat-run/kernel';
|
|
3
|
+
import type { ScopeHostFixture } from './scope-host-suite.js';
|
|
4
|
+
/**
|
|
5
|
+
* #2555: a staff restore or import on a host the control plane runs (`restoreScope`, `importScope`)
|
|
6
|
+
* judges the tenant's roles as the directory stores them NOW against the dump's seats and role rows
|
|
7
|
+
* (`dumpRoleRefusal`), before anything is replaced. A dump holding a seat in a role the directory
|
|
8
|
+
* has since made system-held is refused, with nothing loaded and no scope created; the twin without
|
|
9
|
+
* the seat is admitted. A change the rule admits leaves the dump's seats as they were: nothing is
|
|
10
|
+
* revoked. The refusal is one fixed sentence naming the role key only.
|
|
11
|
+
*/
|
|
12
|
+
export declare function restoreRoleContractSuite(adapterName: string, makeFixture: () => Promise<ScopeHostFixture & {
|
|
13
|
+
roleState(tenantId: TenantId, scopeId: ScopeId): Promise<RoleState>;
|
|
14
|
+
/** Whether this host's dump stores the tenant's role rows (a scope that reads its roles locally). */
|
|
15
|
+
dumpCarriesRoles: boolean;
|
|
16
|
+
}>): void;
|
|
17
|
+
//# sourceMappingURL=restore-role-suite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-role-suite.d.ts","sourceRoot":"","sources":["../src/restore-role-suite.ts"],"names":[],"mappings":"AACA,OAAO,EAAuE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAC3I,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CACxB,gBAAgB,GAAG;IACjB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACpE,qGAAqG;IACrG,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CACF,GACA,IAAI,CAuGN"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { platformActorId, principalId, scopeId, tenantId } from '@substrat-run/contracts';
|
|
3
|
+
import { ulid } from '@substrat-run/kernel';
|
|
4
|
+
/**
|
|
5
|
+
* #2555: a staff restore or import on a host the control plane runs (`restoreScope`, `importScope`)
|
|
6
|
+
* judges the tenant's roles as the directory stores them NOW against the dump's seats and role rows
|
|
7
|
+
* (`dumpRoleRefusal`), before anything is replaced. A dump holding a seat in a role the directory
|
|
8
|
+
* has since made system-held is refused, with nothing loaded and no scope created; the twin without
|
|
9
|
+
* the seat is admitted. A change the rule admits leaves the dump's seats as they were: nothing is
|
|
10
|
+
* revoked. The refusal is one fixed sentence naming the role key only.
|
|
11
|
+
*/
|
|
12
|
+
export function restoreRoleContractSuite(adapterName, makeFixture) {
|
|
13
|
+
describe(`a restore judges the directory's roles against the dump's seats (#2555): ${adapterName}`, () => {
|
|
14
|
+
let fixture;
|
|
15
|
+
const staff = platformActorId.parse(ulid());
|
|
16
|
+
const t = tenantId.parse(ulid());
|
|
17
|
+
const alice = principalId.parse(ulid());
|
|
18
|
+
const USE = 'rrs:use';
|
|
19
|
+
const RUN = 'rrs:run';
|
|
20
|
+
const undefinedInDump = (key) => `restore refused: the dump seats role '${key}' but carries no definition of it, so its seats cannot be told from a person's, ` +
|
|
21
|
+
`and the tenant now defines the role for a system principal (K-55); nothing was loaded`;
|
|
22
|
+
/** The refusal of a seat in a role since made system-held: which sentence depends on whether the dump stores the role. */
|
|
23
|
+
const refusalFor = (key) => fixture.dumpCarriesRoles
|
|
24
|
+
? `restore refused: role '${key}', which the dump seats or defines, cannot be applied as the tenant defines it now (K-55); nothing was loaded`
|
|
25
|
+
: undefinedInDump(key);
|
|
26
|
+
const scope = async () => {
|
|
27
|
+
const id = scopeId.parse(ulid());
|
|
28
|
+
await fixture.host.provisionScope(staff, { tenantId: t, scopeId: id, vertical: 'restore-roles' });
|
|
29
|
+
await fixture.host.admin.activateScope(staff, t, id);
|
|
30
|
+
return id;
|
|
31
|
+
};
|
|
32
|
+
const seat = (roleKey, at) => ({ principalId: alice, roleKey, node: { tenantId: t, scopeId: at } });
|
|
33
|
+
const refusalOf = (p) => p.then(() => undefined, (e) => e);
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
fixture = await makeFixture();
|
|
36
|
+
await fixture.host.admin.createTenant(staff, { id: t, slug: `restore-roles-${t.slice(-8).toLowerCase()}`, name: 'Restore roles' });
|
|
37
|
+
});
|
|
38
|
+
afterAll(async () => fixture?.cleanup());
|
|
39
|
+
it('a dump seated in a role since made system-held is refused: nothing loaded, no scope created', async () => {
|
|
40
|
+
const s = await scope();
|
|
41
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'member', permissions: [USE], source: 'vertical' });
|
|
42
|
+
await fixture.host.admin.assignRole(staff, seat('member', s));
|
|
43
|
+
const dump = await fixture.host.admin.exportScope(staff, t, s);
|
|
44
|
+
// The seat is taken back, so the directory may now make the role system-held.
|
|
45
|
+
await fixture.host.admin.unassignRole(staff, seat('member', s));
|
|
46
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'member', permissions: [RUN], source: 'vertical', holder: 'system' });
|
|
47
|
+
const restored = await refusalOf(fixture.host.restoreScope(staff, t, s, dump));
|
|
48
|
+
expect(restored?.message).toBe(refusalFor('member'));
|
|
49
|
+
// The scope is as it was: the seat the dump holds did not come back.
|
|
50
|
+
expect((await fixture.roleState(t, s)).seated).toEqual([]);
|
|
51
|
+
const fresh = scopeId.parse(ulid());
|
|
52
|
+
const imported = await refusalOf(fixture.host.importScope(staff, { tenantId: t, scopeId: fresh, vertical: 'restore-roles' }, dump));
|
|
53
|
+
expect(imported?.message).toBe(refusalFor('member'));
|
|
54
|
+
expect((await fixture.host.admin.getScopeRecord(staff, t, fresh)) ?? null).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
it('the refusal names the role key only: no principal, no tenant', async () => {
|
|
57
|
+
const s = await scope();
|
|
58
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'named', permissions: [USE], source: 'vertical' });
|
|
59
|
+
await fixture.host.admin.assignRole(staff, seat('named', s));
|
|
60
|
+
const dump = await fixture.host.admin.exportScope(staff, t, s);
|
|
61
|
+
await fixture.host.admin.unassignRole(staff, seat('named', s));
|
|
62
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'named', permissions: [RUN], source: 'vertical', holder: 'system' });
|
|
63
|
+
const message = (await refusalOf(fixture.host.restoreScope(staff, t, s, dump)))?.message ?? '';
|
|
64
|
+
expect(message).toBe(refusalFor('named'));
|
|
65
|
+
for (const id of [alice, t, s])
|
|
66
|
+
expect(message).not.toContain(id);
|
|
67
|
+
});
|
|
68
|
+
it('twin: the same change with no seat in the dump is admitted, by restore and by import', async () => {
|
|
69
|
+
const s = await scope();
|
|
70
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'idle', permissions: [USE], source: 'vertical' });
|
|
71
|
+
const dump = await fixture.host.admin.exportScope(staff, t, s);
|
|
72
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'idle', permissions: [RUN], source: 'vertical', holder: 'system' });
|
|
73
|
+
await fixture.host.restoreScope(staff, t, s, dump);
|
|
74
|
+
const fresh = scopeId.parse(ulid());
|
|
75
|
+
await fixture.host.importScope(staff, { tenantId: t, scopeId: fresh, vertical: 'restore-roles' }, dump);
|
|
76
|
+
expect((await fixture.host.admin.getScopeRecord(staff, t, fresh))?.status).toBe('active');
|
|
77
|
+
});
|
|
78
|
+
it('a person’s role the directory changed since is admitted, and the dump’s seat in it is kept', async () => {
|
|
79
|
+
const s = await scope();
|
|
80
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'editor', permissions: [USE], source: 'vertical' });
|
|
81
|
+
await fixture.host.admin.assignRole(staff, seat('editor', s));
|
|
82
|
+
const dump = await fixture.host.admin.exportScope(staff, t, s);
|
|
83
|
+
await fixture.host.admin.unassignRole(staff, seat('editor', s));
|
|
84
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'editor', permissions: [USE, 'rrs:edit'], source: 'vertical' });
|
|
85
|
+
await fixture.host.restoreScope(staff, t, s, dump);
|
|
86
|
+
expect((await fixture.roleState(t, s)).seated).toEqual(['editor']);
|
|
87
|
+
});
|
|
88
|
+
// Where the dump carries no role rows, this is a scope that cannot be restored from its own export (K-55).
|
|
89
|
+
it('a seat in a role stored system-held when the dump was taken: admitted where the dump stores the role, else judged a person’s', async () => {
|
|
90
|
+
const s = await scope();
|
|
91
|
+
await fixture.host.admin.defineRole(staff, t, { key: 'runner', permissions: [RUN], source: 'vertical', holder: 'system' });
|
|
92
|
+
// The platform's door seats a system-held role made only of keys this host does not reserve.
|
|
93
|
+
await fixture.host.admin.assignRole(staff, seat('runner', s));
|
|
94
|
+
const dump = await fixture.host.admin.exportScope(staff, t, s);
|
|
95
|
+
const restored = await refusalOf(fixture.host.restoreScope(staff, t, s, dump));
|
|
96
|
+
if (fixture.dumpCarriesRoles) {
|
|
97
|
+
expect(restored).toBeUndefined();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// A dump with no role rows says nothing about the definition its seat was made under.
|
|
101
|
+
expect(restored?.message).toBe(undefinedInDump('runner'));
|
|
102
|
+
}
|
|
103
|
+
expect((await fixture.roleState(t, s)).seated).toEqual(['runner']);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=restore-role-suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-role-suite.js","sourceRoot":"","sources":["../src/restore-role-suite.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAuBL,KAAK,SAAS,EAuBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAuBL,KAAK,SAAS,EAuBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAqBL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAe9B,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;AAED,oGAAoG;AACpG,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG;IAC5C,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAChF,CAAC,GACD,IAAI,CA+GN;AAuCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,MACrB,OAAO,CAAC,OAAO,CAAC,QACb,SAAS,YACL,MAAM,KACf,OAAO,CAAC,IAAI,CAOd,CAAC;AAEF,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,CAsuYN"}
|
package/dist/scope-host-suite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
|
|
3
3
|
import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, LIST_PAGE_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, isUndecodablePlatformRequest, platformRequestEntry, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, unheldMigrationDigest, withUpstreamReference, } from '@substrat-run/contracts';
|
|
4
|
-
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, withheldOpsFailure, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, WIRE_AUTH_FILTER_TEXT_MAX, } from '@substrat-run/kernel';
|
|
4
|
+
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, withheldOpsFailure, withheldStaffFailure, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, WIRE_AUTH_FILTER_TEXT_MAX, } from '@substrat-run/kernel';
|
|
5
5
|
import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, TEST_LIFECYCLE, testModManifest, victimModManifest, } from './modules.js';
|
|
6
6
|
import { testOperations } from './test-operations.js';
|
|
7
7
|
import { derivedDigestOf, heldMigrations } from './held-migrations.js';
|
|
@@ -2368,6 +2368,21 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2368
2368
|
// The spine: present and flagged system, so the UI can group it apart.
|
|
2369
2369
|
expect(byName.get('_substrat_outbox')?.system).toBe(true);
|
|
2370
2370
|
});
|
|
2371
|
+
it('counts what is in force apart from what a table keeps as tombstones (#2531)', async () => {
|
|
2372
|
+
const key = `introspect-${ulid().toLowerCase()}`;
|
|
2373
|
+
await host.admin.defineRole(staff, t1, { key, permissions: [permissionKey.parse('thing:read')], source: 'vertical' });
|
|
2374
|
+
const seat = { principalId: principalId.parse(ulid()), roleKey: key, node: { tenantId: t1, scopeId: s1 } };
|
|
2375
|
+
const tuples = async () => (await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_tuples');
|
|
2376
|
+
const before = await tuples();
|
|
2377
|
+
expect(before.liveRowCount).toBeDefined();
|
|
2378
|
+
await host.admin.assignRole(staff, seat);
|
|
2379
|
+
expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount + 1 });
|
|
2380
|
+
// A revoke tombstones the seat: the row is kept, and it is no longer in force.
|
|
2381
|
+
await host.admin.unassignRole(staff, seat);
|
|
2382
|
+
expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount });
|
|
2383
|
+
// A table that keeps no tombstones answers no live count.
|
|
2384
|
+
expect((await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_outbox')).not.toHaveProperty('liveRowCount');
|
|
2385
|
+
});
|
|
2371
2386
|
it('reads a bounded page of a table', async () => {
|
|
2372
2387
|
const stub = await host.getScope(alice, t1, s1);
|
|
2373
2388
|
await stub.invoke('test/write-marker', { v: 'page-row' });
|
|
@@ -3126,6 +3141,29 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
3126
3141
|
expect(await rowsOf(ops.elsewhere)).toEqual(before.elsewhere);
|
|
3127
3142
|
await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
|
|
3128
3143
|
});
|
|
3144
|
+
it("#2556: staff's row alone, for a withheld pass that left a tombstoned intent pending, is redacted too", async () => {
|
|
3145
|
+
const kind = `connector:erasure-${ulid()}`;
|
|
3146
|
+
const erased = dataSubjectId.parse(ulid());
|
|
3147
|
+
const spared = dataSubjectId.parse(ulid());
|
|
3148
|
+
const linked = await routeIntent(kind, erased, 'Anna Ek');
|
|
3149
|
+
const unlinked = await routeIntent(kind, spared, 'Bo Lund');
|
|
3150
|
+
// As the drain files it (`withheldStaffFailure`): no tenant, the drained scope, marker first.
|
|
3151
|
+
const transient = (operation, id, error) => host.admin.recordOpsFailure({
|
|
3152
|
+
actor: staff,
|
|
3153
|
+
...withheldStaffFailure({ operation, stage: 'transient', scopeId: s1 }, `${error}; reference = up${ulid().toLowerCase()}`, ulid(), platformIntentFailureMessage(id, 'pending (the call did not complete at its deployment): ')),
|
|
3154
|
+
});
|
|
3155
|
+
const ops = { linked: op('transient-linked'), unlinked: op('transient-unlinked') };
|
|
3156
|
+
await transient(ops.linked, linked, 'HTTP 500: Anna Ek is not on file');
|
|
3157
|
+
await transient(ops.unlinked, unlinked, 'HTTP 500: Bo Lund is not on file');
|
|
3158
|
+
const rowsOf = async (operation) => (await host.admin.listOpsFailures(staff, { operation })).map((r) => [r.tenantId, r.message]);
|
|
3159
|
+
const before = await rowsOf(ops.unlinked);
|
|
3160
|
+
expect((await rowsOf(ops.linked))[0][1]).toMatch(/^\[upstream reference = up[0-9a-z]+\] platform intent .* pending \(the call did not complete at its deployment\): HTTP 500: /);
|
|
3161
|
+
await host.admin.shredSubject(staff, t1, s1, erased);
|
|
3162
|
+
expect(await rowsOf(ops.linked)).toEqual([[null, REDACTED_FAILURE_NOTE]]);
|
|
3163
|
+
expect((await issueOf(ops.linked)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
|
|
3164
|
+
expect(await rowsOf(ops.unlinked)).toEqual(before);
|
|
3165
|
+
await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
|
|
3166
|
+
});
|
|
3129
3167
|
/** Whose an exemplar is, as the directory stores it — not on the issue read shape. */
|
|
3130
3168
|
const exemplarOwnerOf = async (operation) => {
|
|
3131
3169
|
const dump = await host.admin.exportDirectory(staff);
|
|
@@ -7440,7 +7478,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
7440
7478
|
await host.admin.setVerticalListed(staff, slug, true);
|
|
7441
7479
|
await host.admin.promoteVersion(staff, slug, 'prod', first);
|
|
7442
7480
|
expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
|
|
7443
|
-
|
|
7481
|
+
// Seven scopes, a snapshot and three promotions: about 3 s alone, past the 5 s default under CI load (#2576).
|
|
7482
|
+
}, 30_000);
|
|
7444
7483
|
it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
|
|
7445
7484
|
// Listing is the moment OTHER tenants start trusting this code, so the version
|
|
7446
7485
|
// they would install needs a recorded human decision — the auto-admission note
|
|
@@ -9709,8 +9748,22 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9709
9748
|
const at = (time) => `2026-10-24T${time}.000Z`;
|
|
9710
9749
|
/** The whole fleet, one full page: every read names its reach and its limit. */
|
|
9711
9750
|
const fleet = { reach: { fleet: true }, limit: LIST_PAGE_MAX };
|
|
9712
|
-
/**
|
|
9713
|
-
const
|
|
9751
|
+
/** The verticals this block registered: a record is written only beside its vertical's registration. */
|
|
9752
|
+
const registered = new Set();
|
|
9753
|
+
const register = async (slug, ownerTenant) => {
|
|
9754
|
+
await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli', ...(ownerTenant ? { ownerTenant } : {}) });
|
|
9755
|
+
registered.add(slug);
|
|
9756
|
+
};
|
|
9757
|
+
const forget = async (slug) => {
|
|
9758
|
+
await host.admin.deleteVertical(staff, slug);
|
|
9759
|
+
registered.delete(slug);
|
|
9760
|
+
};
|
|
9761
|
+
/** Record `rec` for the script `<vertical>-<script>`, registering the vertical (platform-owned) first when no test did. */
|
|
9762
|
+
const put = async (vertical, script, versionId, rec, time) => {
|
|
9763
|
+
if (!registered.has(vertical))
|
|
9764
|
+
await register(vertical);
|
|
9765
|
+
await host.admin.recordWireAuth(staff, { deploymentRef: `${vertical}-${script}`, verticalSlug: vertical, versionId, record: rec, recordedAt: at(time) });
|
|
9766
|
+
};
|
|
9714
9767
|
it('keeps one record per deployment, the last upload’s, and lists one vertical’s by deployment', async () => {
|
|
9715
9768
|
const [a, b] = [slugOf('wire-a'), slugOf('wire-b')];
|
|
9716
9769
|
await put(a, 'serving', 'v1', record({ audience: `${a}-serving` }), '10:00:00');
|
|
@@ -9825,26 +9878,25 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9825
9878
|
const [owner, other] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
|
|
9826
9879
|
await host.admin.createTenant(staff, { id: owner, slug: `wire-own-${owner.slice(-6).toLowerCase()}`, name: 'Wire Owner' });
|
|
9827
9880
|
await host.admin.createTenant(staff, { id: other, slug: `wire-oth-${other.slice(-6).toLowerCase()}`, name: 'Wire Other' });
|
|
9828
|
-
const [mine, theirs, platform
|
|
9829
|
-
await
|
|
9830
|
-
await
|
|
9831
|
-
await
|
|
9832
|
-
for (const [i, slug] of [mine, theirs, platform
|
|
9881
|
+
const [mine, theirs, platform] = [slugOf('wire-mine'), slugOf('wire-theirs'), slugOf('wire-plat')];
|
|
9882
|
+
await register(mine, owner);
|
|
9883
|
+
await register(theirs, other);
|
|
9884
|
+
await register(platform);
|
|
9885
|
+
for (const [i, slug] of [mine, theirs, platform].entries()) {
|
|
9833
9886
|
await put(slug, 'serving', null, record(), `16:00:0${i}`);
|
|
9834
9887
|
await put(slug, 'archive', 'v1', record(), `16:00:1${i}`);
|
|
9835
9888
|
}
|
|
9836
9889
|
const refs = async (reach, verticalSlug) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, ...(verticalSlug ? { verticalSlug } : {}) }))
|
|
9837
9890
|
.map((r) => r.deploymentRef)
|
|
9838
|
-
.filter((ref) => [mine, theirs, platform
|
|
9891
|
+
.filter((ref) => [mine, theirs, platform].some((slug) => ref.startsWith(`${slug}-`)));
|
|
9839
9892
|
expect(await refs({ tenantId: owner })).toEqual([`${mine}-archive`, `${mine}-serving`]);
|
|
9840
9893
|
expect(await refs({ tenantId: other })).toEqual([`${theirs}-archive`, `${theirs}-serving`]);
|
|
9841
9894
|
// Naming another tenant's vertical inside one tenant's reach reaches nothing, not that vertical.
|
|
9842
9895
|
expect(await refs({ tenantId: owner }, theirs)).toEqual([]);
|
|
9843
9896
|
expect(await refs({ tenantId: owner }, mine)).toEqual([`${mine}-archive`, `${mine}-serving`]);
|
|
9844
|
-
// A tenant that owns nothing reaches nothing; a vertical no tenant owns
|
|
9845
|
-
// know, is reached by the fleet alone.
|
|
9897
|
+
// A tenant that owns nothing reaches nothing; a vertical no tenant owns is reached by the fleet alone.
|
|
9846
9898
|
expect(await refs({ tenantId: tenantId.parse(ulid()) })).toEqual([]);
|
|
9847
|
-
for (const slug of [mine, theirs, platform
|
|
9899
|
+
for (const slug of [mine, theirs, platform]) {
|
|
9848
9900
|
expect(await refs({ fleet: true }, slug)).toEqual([`${slug}-archive`, `${slug}-serving`]);
|
|
9849
9901
|
}
|
|
9850
9902
|
// A page inside the reach pages over the tenant's records only.
|
|
@@ -9891,7 +9943,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9891
9943
|
const owner = tenantId.parse(ulid());
|
|
9892
9944
|
await host.admin.createTenant(staff, { id: owner, slug: `wire-log-${owner.slice(-6).toLowerCase()}`, name: 'Wire Log' });
|
|
9893
9945
|
const mine = slugOf('wire-logged');
|
|
9894
|
-
await
|
|
9946
|
+
await register(mine, owner);
|
|
9895
9947
|
await put(mine, 'serving', null, record(), '17:00:00');
|
|
9896
9948
|
// The reach given last, as a caller may spell it: the log still names it first.
|
|
9897
9949
|
await host.admin.listWireAuth(staff, { limit: 5, verticalSlug: mine, reach: { tenantId: owner } });
|
|
@@ -9919,30 +9971,119 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9919
9971
|
await host.admin.createTenant(staff, { id: second, slug: `wire-snd-${second.slice(-6).toLowerCase()}`, name: 'Wire Second' });
|
|
9920
9972
|
const slug = slugOf('wire-owned');
|
|
9921
9973
|
const refsOf = async (owner) => (await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => r.deploymentRef);
|
|
9922
|
-
//
|
|
9974
|
+
// The owner reads what was recorded while it held the slug.
|
|
9975
|
+
await register(slug, first);
|
|
9923
9976
|
await put(slug, 'archive', 'v0', record(), '18:00:00');
|
|
9924
|
-
expect(await refsOf(first)).toEqual([]);
|
|
9925
|
-
await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
|
|
9926
9977
|
expect(await refsOf(first)).toEqual([`${slug}-archive`]);
|
|
9927
9978
|
await put(slug, 'serving', 'v1', record(), '18:00:01');
|
|
9928
9979
|
expect(await refsOf(first)).toEqual([`${slug}-archive`, `${slug}-serving`]);
|
|
9929
9980
|
// An owner changes by deleting the vertical, which takes its records, and registering it again:
|
|
9930
9981
|
// the tenant that held it reads nothing after, the new owner reads only what was recorded since.
|
|
9931
|
-
await
|
|
9932
|
-
await
|
|
9982
|
+
await forget(slug);
|
|
9983
|
+
await register(slug, second);
|
|
9933
9984
|
expect(await refsOf(first)).toEqual([]);
|
|
9934
9985
|
expect(await refsOf(second)).toEqual([]);
|
|
9935
9986
|
await put(slug, 'serving', 'v2', record(), '18:00:02');
|
|
9936
9987
|
expect(await refsOf(first)).toEqual([]);
|
|
9937
9988
|
expect(await refsOf(second)).toEqual([`${slug}-serving`]);
|
|
9938
9989
|
});
|
|
9990
|
+
it('writes no record, and no admin row, for a vertical the registry does not hold', async () => {
|
|
9991
|
+
const never = slugOf('wire-unreg');
|
|
9992
|
+
const gone = slugOf('wire-gone');
|
|
9993
|
+
await put(gone, 'serving', 'v1', record(), '20:00:00');
|
|
9994
|
+
await forget(gone);
|
|
9995
|
+
for (const slug of [never, gone]) {
|
|
9996
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { deploymentRef: `${slug}-serving`, verticalSlug: slug, versionId: 'v2', record: record(), recordedAt: at('20:00:01') }), 'not_found');
|
|
9997
|
+
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: slug })).toEqual([]);
|
|
9998
|
+
}
|
|
9999
|
+
const rows = (await host.admin.auditLog(staff, { action: 'recordWireAuth' })).filter((r) => r.vertical === never || r.vertical === gone);
|
|
10000
|
+
// Only the one write made while `gone` was registered.
|
|
10001
|
+
expect(rows.map((r) => r.after.versionId)).toEqual(['v1']);
|
|
10002
|
+
// Registered again, the slug's records start empty: nothing written while unregistered survives.
|
|
10003
|
+
await register(gone);
|
|
10004
|
+
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: gone })).toEqual([]);
|
|
10005
|
+
});
|
|
10006
|
+
it('keeps a walk’s hold on a record, by binding name, and refuses a hold it cannot read', async () => {
|
|
10007
|
+
const a = slugOf('wire-hold');
|
|
10008
|
+
const hold = { reason: 'verify', bindings: ['SCOPE', 'SUBSTRAT_WIRE_STATE'], at: at('22:00:00') };
|
|
10009
|
+
await put(a, 'archive', 'v1', { ...record(), walkHold: hold }, '22:00:00');
|
|
10010
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(hold);
|
|
10011
|
+
// The next upload of the script writes a record without it.
|
|
10012
|
+
await put(a, 'archive', 'v1', record(), '22:00:01');
|
|
10013
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
|
|
10014
|
+
const good = { deploymentRef: `${a}-archive`, verticalSlug: a, versionId: 'v1', recordedAt: at('22:00:02') };
|
|
10015
|
+
for (const walkHold of [
|
|
10016
|
+
{ ...hold, reason: 'because' },
|
|
10017
|
+
{ ...hold, at: 'yesterday' },
|
|
10018
|
+
{ ...hold, bindings: [''] },
|
|
10019
|
+
{ ...hold, bindings: Array.from({ length: 65 }, (_, i) => `B${i}`) },
|
|
10020
|
+
{ ...hold, value: 'not a field' },
|
|
10021
|
+
// The count of names past the cap is a positive whole number when given.
|
|
10022
|
+
{ ...hold, more: 0 },
|
|
10023
|
+
{ ...hold, more: 1.5 },
|
|
10024
|
+
]) {
|
|
10025
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), walkHold } }), 'validation_failed');
|
|
10026
|
+
}
|
|
10027
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
|
|
10028
|
+
// A hold of more names than it keeps: the first ones, and how many more.
|
|
10029
|
+
const many = { ...hold, bindings: Array.from({ length: 64 }, (_, i) => `B${i}`), more: 6 };
|
|
10030
|
+
await put(a, 'archive', 'v1', { ...record(), walkHold: many }, '22:00:02');
|
|
10031
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(many);
|
|
10032
|
+
await put(a, 'archive', 'v1', record(), '22:00:02');
|
|
10033
|
+
// `unverified`, the walk's record of a script it could not confirm: lists claimed by none, and
|
|
10034
|
+
// only beside a hold. Without the hold, or beside lists, it is refused.
|
|
10035
|
+
const unverified = withheld('unverified');
|
|
10036
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: unverified }), 'validation_failed');
|
|
10037
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), withheld: 'unverified', walkHold: hold } }), 'validation_failed');
|
|
10038
|
+
await put(a, 'archive', 'v1', { ...unverified, walkHold: hold }, '22:00:03');
|
|
10039
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record).toMatchObject({ withheld: 'unverified', state: 'legacy-only', routerKids: [], walkHold: hold });
|
|
10040
|
+
});
|
|
10041
|
+
it('audits a pass of the fleet walk as walkWireAuth, naming scripts and reasons, never a value', async () => {
|
|
10042
|
+
const entry = {
|
|
10043
|
+
apply: true,
|
|
10044
|
+
confirm: false,
|
|
10045
|
+
from: null,
|
|
10046
|
+
next: 'acme/crm|01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
|
10047
|
+
walked: 3,
|
|
10048
|
+
updated: ['acme-crm'],
|
|
10049
|
+
held: ['acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz'],
|
|
10050
|
+
released: [],
|
|
10051
|
+
skipped: [{ deploymentRef: 'acme-crm-01hyyyyyyyyyyyyyyyyyyyyyyy', reason: 'no-entry' }],
|
|
10052
|
+
failed: [{ deploymentRef: 'acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz', reason: 'verify' }],
|
|
10053
|
+
};
|
|
10054
|
+
await host.admin.recordWireWalk(staff, entry);
|
|
10055
|
+
const rows = (await host.admin.auditLog(staff, { action: 'walkWireAuth' })).filter((r) => r.after.next === entry.next);
|
|
10056
|
+
expect(rows).toHaveLength(1);
|
|
10057
|
+
expect(rows[0].actor).toBe(staff);
|
|
10058
|
+
expect(rows[0].after).toEqual(entry);
|
|
10059
|
+
});
|
|
10060
|
+
it('restores the owners and the records together, from one dump', async () => {
|
|
10061
|
+
const [first, second] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
|
|
10062
|
+
await host.admin.createTenant(staff, { id: first, slug: `wire-rst-${first.slice(-6).toLowerCase()}`, name: 'Wire Restore' });
|
|
10063
|
+
const slug = slugOf('wire-restore');
|
|
10064
|
+
await register(slug, first);
|
|
10065
|
+
await put(slug, 'serving', 'v1', record(), '21:00:00');
|
|
10066
|
+
const backup = await host.admin.exportDirectory(staff);
|
|
10067
|
+
// Diverge past the copy: the vertical deleted (its records with it) and taken by another tenant.
|
|
10068
|
+
await host.admin.createTenant(staff, { id: second, slug: `wire-rs2-${second.slice(-6).toLowerCase()}`, name: 'Wire Restore 2' });
|
|
10069
|
+
await forget(slug);
|
|
10070
|
+
await register(slug, second);
|
|
10071
|
+
await put(slug, 'archive', 'v2', record(), '21:00:01');
|
|
10072
|
+
await host.admin.restoreDirectory(staff, backup);
|
|
10073
|
+
registered.add(slug);
|
|
10074
|
+
// The copy's owner reads the copy's records, and nothing of the divergence came back with it.
|
|
10075
|
+
const reads = async (reach) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => [r.deploymentRef, r.versionId]);
|
|
10076
|
+
expect(await reads({ tenantId: first })).toEqual([[`${slug}-serving`, 'v1']]);
|
|
10077
|
+
expect(await reads({ tenantId: second })).toEqual([]);
|
|
10078
|
+
expect(await reads({ fleet: true })).toEqual([[`${slug}-serving`, 'v1']]);
|
|
10079
|
+
});
|
|
9939
10080
|
it('forgets a vertical’s records with the vertical', async () => {
|
|
9940
10081
|
const a = slugOf('wire-del');
|
|
9941
|
-
await
|
|
10082
|
+
await register(a);
|
|
9942
10083
|
await put(a, 'serving', null, record(), '13:00:00');
|
|
9943
10084
|
const kept = slugOf('wire-kept');
|
|
9944
10085
|
await put(kept, 'serving', null, record(), '13:00:01');
|
|
9945
|
-
await
|
|
10086
|
+
await forget(a);
|
|
9946
10087
|
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
|
|
9947
10088
|
// Another vertical's record is not touched.
|
|
9948
10089
|
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: kept })).toHaveLength(1);
|
|
@@ -10638,6 +10779,16 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
10638
10779
|
name: 'Impostor Co',
|
|
10639
10780
|
}), 'conflict', /already taken/);
|
|
10640
10781
|
});
|
|
10782
|
+
it('words the tenant slug refusal without the tenant that holds the slug (#2556)', async () => {
|
|
10783
|
+
const holder = tenantId.parse(ulid());
|
|
10784
|
+
const slug = `held-${holder.toLowerCase()}`;
|
|
10785
|
+
await host.admin.createTenant(staff, { id: holder, slug, name: 'Holder Co' });
|
|
10786
|
+
const taken = await host.admin
|
|
10787
|
+
.createTenant(staff, { id: tenantId.parse(ulid()), slug, name: 'Impostor Co' })
|
|
10788
|
+
.then(() => null, (e) => e);
|
|
10789
|
+
expect(taken?.message).toBe(`tenant slug '${slug}' is already taken (slugs are unique)`);
|
|
10790
|
+
expect(taken?.message).not.toContain(holder);
|
|
10791
|
+
});
|
|
10641
10792
|
it('enumerates the scopes under a tenant, and filters by status (§4.5)', async () => {
|
|
10642
10793
|
const all = await host.admin.listScopes(staff, { tenantId: t5 });
|
|
10643
10794
|
expect(all.length).toBeGreaterThanOrEqual(2);
|