@substrat-run/contract-tests 0.87.0 → 0.89.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/concurrency-suite.d.ts +3 -0
- package/dist/concurrency-suite.d.ts.map +1 -0
- package/dist/concurrency-suite.js +228 -0
- package/dist/concurrency-suite.js.map +1 -0
- package/dist/conformance.d.ts +85 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +17 -0
- package/dist/conformance.js.map +1 -0
- package/dist/entity-check-plan.d.ts +138 -0
- package/dist/entity-check-plan.d.ts.map +1 -0
- package/dist/entity-check-plan.js +181 -0
- package/dist/entity-check-plan.js.map +1 -0
- package/dist/entity-check-suite.d.ts +3 -90
- package/dist/entity-check-suite.d.ts.map +1 -1
- package/dist/entity-check-suite.js +42 -71
- package/dist/entity-check-suite.js.map +1 -1
- package/dist/entity-version-suite.d.ts +3 -0
- package/dist/entity-version-suite.d.ts.map +1 -0
- package/dist/entity-version-suite.js +160 -0
- package/dist/entity-version-suite.js.map +1 -0
- package/dist/idempotency-suite.d.ts +3 -0
- package/dist/idempotency-suite.d.ts.map +1 -0
- package/dist/idempotency-suite.js +243 -0
- package/dist/idempotency-suite.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/input-parse-suite.d.ts +3 -0
- package/dist/input-parse-suite.d.ts.map +1 -0
- package/dist/input-parse-suite.js +149 -0
- package/dist/input-parse-suite.js.map +1 -0
- package/dist/modules.d.ts +482 -160
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +367 -2
- package/dist/modules.js.map +1 -1
- package/dist/node-only-suite.d.ts +28 -0
- package/dist/node-only-suite.d.ts.map +1 -1
- package/dist/node-only-suite.js +43 -1
- package/dist/node-only-suite.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +125 -0
- package/dist/permission-suite.js.map +1 -1
- package/dist/timeline-suite.d.ts +3 -0
- package/dist/timeline-suite.d.ts.map +1 -0
- package/dist/timeline-suite.js +223 -0
- package/dist/timeline-suite.js.map +1 -0
- package/package.json +11 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-suite.d.ts","sourceRoot":"","sources":["../src/concurrency-suite.ts"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAK9D,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAC3C,IAAI,CAgPN"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract suite for optimistic concurrency — `If-Match` and the 412 (#129).
|
|
3
|
+
*
|
|
4
|
+
* What it pins: **the comparison belongs to the HOST, and it happens inside the
|
|
5
|
+
* write's own transaction.** Everything else about this feature is a projection.
|
|
6
|
+
* The header names are a wire detail `vertical-host` owns, the declaration is a
|
|
7
|
+
* model detail `contracts` owns, and neither is what makes a write safe — that is
|
|
8
|
+
* this: a version read and a write committed with nothing able to interleave.
|
|
9
|
+
*
|
|
10
|
+
* Behavioural, for the reason the entity-version suite gives one screen over: an
|
|
11
|
+
* adapter that reads correctly can still be wrong against a real database, and a
|
|
12
|
+
* string comparison on the emitted SQL would call it a pass. So every case here
|
|
13
|
+
* provisions a real scope, commits real writes, and asks a real question.
|
|
14
|
+
*
|
|
15
|
+
* The cases that matter are not the happy path:
|
|
16
|
+
*
|
|
17
|
+
* - **A refused write leaves nothing behind.** The precondition runs before the
|
|
18
|
+
* guards and before the handler, so a 412 must not be a partial write with an
|
|
19
|
+
* error attached.
|
|
20
|
+
* - **An `If-Match` on an operation that declares nothing is REFUSED.** Ignoring
|
|
21
|
+
* it would leave a caller believing its write was serialised when nothing was
|
|
22
|
+
* compared — the failure this whole mechanism exists to prevent, arrived at
|
|
23
|
+
* through the mechanism itself.
|
|
24
|
+
* - **A guarded write that emits nothing does not move the tag.** The documented
|
|
25
|
+
* hole, pinned here so it stays a known property of the spine rather than
|
|
26
|
+
* something an adapter is tempted to paper over by fabricating a version.
|
|
27
|
+
*/
|
|
28
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
29
|
+
import { etagOf, permissionKey, platformActorId, principalId, scopeId, tenantId, errorCodeOf, } from '@substrat-run/contracts';
|
|
30
|
+
import { ulid } from '@substrat-run/kernel';
|
|
31
|
+
import { concurrencyMod } from './modules.js';
|
|
32
|
+
const CONC_USE = permissionKey.parse('conc:use');
|
|
33
|
+
export function concurrencyContractSuite(adapterName, makeFixture) {
|
|
34
|
+
describe(`optimistic concurrency (If-Match): ${adapterName}`, () => {
|
|
35
|
+
let fixture;
|
|
36
|
+
let host;
|
|
37
|
+
let stub;
|
|
38
|
+
/** Every tag the host reported, newest last — the `ETag` an HTTP layer would set. */
|
|
39
|
+
let tags = [];
|
|
40
|
+
const t1 = tenantId.parse(ulid());
|
|
41
|
+
const s1 = scopeId.parse(ulid());
|
|
42
|
+
const alice = principalId.parse(ulid());
|
|
43
|
+
const staff = platformActorId.parse(ulid());
|
|
44
|
+
/** The version the host last reported — what a client would be holding. */
|
|
45
|
+
const lastTag = () => {
|
|
46
|
+
const version = tags.at(-1);
|
|
47
|
+
if (typeof version !== 'string')
|
|
48
|
+
throw new Error('no version was reported');
|
|
49
|
+
return version;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The reply channel an HTTP mount supplies, collected rather than overwritten
|
|
53
|
+
* so a case can assert a version was NOT reported as easily as that one was.
|
|
54
|
+
*/
|
|
55
|
+
const sink = { onEntityVersion: (version) => tags.push(version) };
|
|
56
|
+
const update = (thingId, label, ifMatch) => stub.invoke('conc/update', { thingId, label }, { ...sink, ...(ifMatch === undefined ? {} : { ifMatch }) });
|
|
57
|
+
const read = (thingId, ifMatch) => stub.invoke('conc/read', { thingId }, { ...sink, ...(ifMatch === undefined ? {} : { ifMatch }) });
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
fixture = await makeFixture();
|
|
60
|
+
host = fixture.host;
|
|
61
|
+
host.registerModule(concurrencyMod);
|
|
62
|
+
await host.admin.createTenant(staff, { id: t1, slug: 'conc-tenant', name: 'Conc Tenant' });
|
|
63
|
+
await host.admin.grantEntitlement(staff, t1, 'conc');
|
|
64
|
+
await host.admin.defineRole(staff, t1, {
|
|
65
|
+
key: 'conc-admin',
|
|
66
|
+
permissions: [CONC_USE],
|
|
67
|
+
source: 'vertical',
|
|
68
|
+
});
|
|
69
|
+
await host.admin.assignRole(staff, {
|
|
70
|
+
principalId: alice,
|
|
71
|
+
roleKey: 'conc-admin',
|
|
72
|
+
node: { tenantId: t1, scopeId: null },
|
|
73
|
+
});
|
|
74
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s1, vertical: 'conc-vertical' });
|
|
75
|
+
await host.admin.activateScope(staff, t1, s1);
|
|
76
|
+
stub = await host.getScope(alice, t1, s1);
|
|
77
|
+
});
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
await fixture.cleanup();
|
|
80
|
+
});
|
|
81
|
+
it('reports a version for a guarded operation, and none for an unguarded one', async () => {
|
|
82
|
+
tags = [];
|
|
83
|
+
await stub.invoke('conc/unguarded', { thingId: 'u1' }, sink);
|
|
84
|
+
// Nothing declared, nothing read: an operation that opted out must not pay
|
|
85
|
+
// for a spine query on every invocation.
|
|
86
|
+
expect(tags).toEqual([]);
|
|
87
|
+
await read(`t-${ulid()}`);
|
|
88
|
+
expect(tags).toHaveLength(1);
|
|
89
|
+
});
|
|
90
|
+
it('answers a never-written entity with a null version', async () => {
|
|
91
|
+
tags = [];
|
|
92
|
+
await read(`t-${ulid()}`);
|
|
93
|
+
// Absence, not a throw. A read is how a client learns there is nothing to
|
|
94
|
+
// hold a tag for yet, which is what makes the create-vs-update distinction
|
|
95
|
+
// visible to it at all.
|
|
96
|
+
expect(tags).toEqual([null]);
|
|
97
|
+
});
|
|
98
|
+
it('admits a write whose tag is current, and hands back the one it created', async () => {
|
|
99
|
+
const id = `t-${ulid()}`;
|
|
100
|
+
tags = [];
|
|
101
|
+
await update(id, 'first');
|
|
102
|
+
const afterFirst = lastTag();
|
|
103
|
+
expect(afterFirst).not.toBeNull();
|
|
104
|
+
await update(id, 'second', etagOf(afterFirst));
|
|
105
|
+
const afterSecond = lastTag();
|
|
106
|
+
// The tag returned describes the row as THIS write left it — not as the
|
|
107
|
+
// caller found it. A client that echoes what it sent would loop forever on
|
|
108
|
+
// its own stale value.
|
|
109
|
+
expect(afterSecond).not.toBe(afterFirst);
|
|
110
|
+
expect(afterSecond > afterFirst).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('refuses a write whose tag has moved, with `precondition_failed`', async () => {
|
|
113
|
+
const id = `t-${ulid()}`;
|
|
114
|
+
await update(id, 'first');
|
|
115
|
+
const stale = etagOf(lastTag());
|
|
116
|
+
// A concurrent writer lands between the read and the write.
|
|
117
|
+
await update(id, 'from the other tab');
|
|
118
|
+
await expect(update(id, 'from the stale tab', stale)).rejects.toSatisfy((err) => errorCodeOf(err) === 'precondition_failed');
|
|
119
|
+
});
|
|
120
|
+
it('leaves nothing behind when it refuses', async () => {
|
|
121
|
+
const id = `t-${ulid()}`;
|
|
122
|
+
await update(id, 'first');
|
|
123
|
+
const stale = etagOf(lastTag());
|
|
124
|
+
await update(id, 'second');
|
|
125
|
+
const beforeRefusal = lastTag();
|
|
126
|
+
tags = [];
|
|
127
|
+
await expect(update(id, 'never lands', stale)).rejects.toThrow();
|
|
128
|
+
// Two halves of one claim. The row still holds what the winning write left
|
|
129
|
+
// — the refused handler never ran, so it wrote nothing to roll back — and
|
|
130
|
+
// no tag was reported, because a version that did not survive its
|
|
131
|
+
// transaction is not one any client may hold.
|
|
132
|
+
expect(tags).toEqual([]);
|
|
133
|
+
await read(id);
|
|
134
|
+
expect(lastTag()).toBe(beforeRefusal);
|
|
135
|
+
});
|
|
136
|
+
it('refuses `If-Match` against an entity that has no version yet', async () => {
|
|
137
|
+
// Nothing has ever been emitted about this id, so there is no version and
|
|
138
|
+
// nothing the caller could legitimately be holding. Admitting it because
|
|
139
|
+
// "there is nothing to conflict with" would make a tag from a rolled-back
|
|
140
|
+
// write — or from another entity entirely — a free pass.
|
|
141
|
+
await expect(update(`t-${ulid()}`, 'x', etagOf(ulid()))).rejects.toSatisfy((err) => errorCodeOf(err) === 'precondition_failed');
|
|
142
|
+
});
|
|
143
|
+
it('honours `If-Match: *` as "must already exist"', async () => {
|
|
144
|
+
const id = `t-${ulid()}`;
|
|
145
|
+
// RFC 9110 §13.1.1: `*` means any current representation. For us that is
|
|
146
|
+
// "has a version at all", which makes it the update-only guard — the
|
|
147
|
+
// counterpart to `If-None-Match: *` for create-only.
|
|
148
|
+
await expect(update(id, 'first', '*')).rejects.toSatisfy((err) => errorCodeOf(err) === 'precondition_failed');
|
|
149
|
+
await update(id, 'first');
|
|
150
|
+
await expect(update(id, 'second', '*')).resolves.toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
it('accepts a tag from a comma-separated list, and never a weak one', async () => {
|
|
153
|
+
const id = `t-${ulid()}`;
|
|
154
|
+
await update(id, 'first');
|
|
155
|
+
const current = lastTag();
|
|
156
|
+
// A list is the client saying "any of these will do" — §13.1.1 permits it,
|
|
157
|
+
// and a client that has followed a redirect legitimately holds two.
|
|
158
|
+
await expect(update(id, 'second', `"${ulid()}", ${etagOf(current)}`)).resolves.toBeDefined();
|
|
159
|
+
// A weak validator means "semantically equivalent", which is a judgement no
|
|
160
|
+
// generic layer is entitled to make about a domain entity. §13.1.1 requires
|
|
161
|
+
// the strong comparison for `If-Match`, so this must refuse.
|
|
162
|
+
await expect(update(id, 'third', `W/${etagOf(lastTag())}`)).rejects.toSatisfy((err) => errorCodeOf(err) === 'precondition_failed');
|
|
163
|
+
});
|
|
164
|
+
it('refuses `If-Match` on an operation that declares no concurrency', async () => {
|
|
165
|
+
// The whole point of refusing rather than ignoring. A caller sending this
|
|
166
|
+
// believes its write is serialised; a 200 would leave that belief intact
|
|
167
|
+
// while nothing was compared. It is not a `precondition_failed` — the
|
|
168
|
+
// precondition was never evaluated — it is a caller error.
|
|
169
|
+
await expect(stub.invoke('conc/unguarded', { thingId: 'u1' }, { ifMatch: etagOf(ulid()) })).rejects.toThrow(/declares no `concurrency`/);
|
|
170
|
+
});
|
|
171
|
+
it('refuses a guarded operation whose id field the caller omitted', async () => {
|
|
172
|
+
// `conc/keyless` declares `idFrom: 'thingId'` over an optional field. There
|
|
173
|
+
// is no row to read, so there is nothing to compare — and skipping the
|
|
174
|
+
// comparison is indistinguishable from passing it.
|
|
175
|
+
await expect(stub.invoke('conc/keyless', {}, { ifMatch: '*' })).rejects.toThrow(/carries no such id/);
|
|
176
|
+
});
|
|
177
|
+
it('answers a permission denial ahead of the precondition', async () => {
|
|
178
|
+
const id = `t-${ulid()}`;
|
|
179
|
+
await update(id, 'first');
|
|
180
|
+
const stale = etagOf(lastTag());
|
|
181
|
+
await update(id, 'moved');
|
|
182
|
+
// `conc/forbidden` checks a key nobody holds. The caller's tag is stale, so
|
|
183
|
+
// BOTH refusals apply — and the one that must win is the permission.
|
|
184
|
+
//
|
|
185
|
+
// The other order makes a guarded operation a version oracle: a principal
|
|
186
|
+
// with no permission on the entity sends `If-Match: *` and learns whether it
|
|
187
|
+
// exists, or sends a tag and learns whether it has changed, all without ever
|
|
188
|
+
// being allowed to read it. The precondition therefore snapshots the version
|
|
189
|
+
// before the handler and compares AFTER it, so the handler's own
|
|
190
|
+
// `assertAllowed` is what answers first.
|
|
191
|
+
//
|
|
192
|
+
// Found by driving Callout's two-dispatcher scenario over real HTTP as a
|
|
193
|
+
// principal who lacked `facility:manage`: it answered 412 where it owed 403.
|
|
194
|
+
await expect(stub.invoke('conc/forbidden', { thingId: id }, { ...sink, ifMatch: stale })).rejects.toSatisfy((err) => errorCodeOf(err) === 'permission_denied');
|
|
195
|
+
});
|
|
196
|
+
it('does not move the tag for a guarded write that emits nothing', async () => {
|
|
197
|
+
const id = `t-${ulid()}`;
|
|
198
|
+
await update(id, 'first');
|
|
199
|
+
const before = lastTag();
|
|
200
|
+
await stub.invoke('conc/silent', { thingId: id, label: 'quietly changed' }, { ...sink, ifMatch: etagOf(before) });
|
|
201
|
+
// The documented hole, and the reason `assertConcurrencyMovesVersion`
|
|
202
|
+
// exists in the model layer rather than here. The write landed, the version
|
|
203
|
+
// did not move, and the stale tag is still accepted:
|
|
204
|
+
expect(lastTag()).toBe(before);
|
|
205
|
+
await expect(stub.invoke('conc/silent', { thingId: id, label: 'again' }, { ...sink, ifMatch: etagOf(before) })).resolves.toBeDefined();
|
|
206
|
+
// An adapter must NOT try to fix this by inventing a version. The spine
|
|
207
|
+
// records what was announced; a host that fabricated a tag here would be
|
|
208
|
+
// reporting a change no consumer, projection or audit ever saw.
|
|
209
|
+
});
|
|
210
|
+
it('serialises two writers who both hold the same tag', async () => {
|
|
211
|
+
const id = `t-${ulid()}`;
|
|
212
|
+
await update(id, 'first');
|
|
213
|
+
const shared = etagOf(lastTag());
|
|
214
|
+
// The scenario in one assertion: both read, both submit, and exactly one
|
|
215
|
+
// may land. Invokes are serialised per scope, so this pins the OUTCOME —
|
|
216
|
+
// that the second is refused on the version rather than admitted by an
|
|
217
|
+
// ordering accident — rather than pretending to test parallelism.
|
|
218
|
+
const results = await Promise.allSettled([
|
|
219
|
+
update(id, 'writer A', shared),
|
|
220
|
+
update(id, 'writer B', shared),
|
|
221
|
+
]);
|
|
222
|
+
expect(results.filter((r) => r.status === 'fulfilled')).toHaveLength(1);
|
|
223
|
+
const refused = results.find((r) => r.status === 'rejected');
|
|
224
|
+
expect(errorCodeOf(refused.reason)).toBe('precondition_failed');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=concurrency-suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-suite.js","sourceRoot":"","sources":["../src/concurrency-suite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a package CLAIMS about its entity checks, in one importable object (#866).
|
|
3
|
+
*
|
|
4
|
+
* The conformance kit next door proves a handler honours the check its operation
|
|
5
|
+
* declared. That proof runs inside vitest and vanishes with the process. The
|
|
6
|
+
* trust page kernel-design §11.2 committed to — *"the §10 table above, run
|
|
7
|
+
* adversarially; results published"* — needs the same facts at emit time, and a
|
|
8
|
+
* tool cannot import a test file: its top-level `describe` throws outside a
|
|
9
|
+
* runner.
|
|
10
|
+
*
|
|
11
|
+
* So the claim moves out of the test and into `test/conformance.ts`, which the
|
|
12
|
+
* test file and `tools/conformance-emit.mts` both import. That is the whole
|
|
13
|
+
* design, and the reason for it is the one PERMISSIONS.md is built on: an
|
|
14
|
+
* artifact rendered from a SECOND copy of the facts is an artifact that can
|
|
15
|
+
* disagree with what runs. Here it cannot — `planEntityCheckCoverage` is handed
|
|
16
|
+
* the same `inputs` and `refEntityType` the suite is driven with, so the
|
|
17
|
+
* covered/uncovered partition in `CONFORMANCE.md` is the partition the suite
|
|
18
|
+
* asserts, by construction.
|
|
19
|
+
*
|
|
20
|
+
* ## Three kinds, because there are three strengths of evidence
|
|
21
|
+
*
|
|
22
|
+
* A report that renders all three as "assessed" would be the overclaim this
|
|
23
|
+
* whole thread exists to avoid. They are not equal:
|
|
24
|
+
*
|
|
25
|
+
* - `driven` — the operation set is declared AND the pair runs against the
|
|
26
|
+
* handler. A wrong implementation fails. This is evidence.
|
|
27
|
+
* - `declared` — the operation set is declared and its plan is empty: nothing
|
|
28
|
+
* narrows, and the declaration is what says so. Nothing is driven because
|
|
29
|
+
* there is nothing to drive, and the day an operation narrows, the plan stops
|
|
30
|
+
* being empty and the assertion goes red.
|
|
31
|
+
* - `asserted` — there is no declared operation set, so the claim is a lexical
|
|
32
|
+
* tripwire over the module's own source (`nodeOnlySuite`). It proves an
|
|
33
|
+
* absence on the obvious path and nothing more. Weakest of the three, and the
|
|
34
|
+
* report says so rather than letting a reader assume otherwise.
|
|
35
|
+
*
|
|
36
|
+
* The kind is stamped by the helper rather than written by the caller: a package
|
|
37
|
+
* cannot label itself `driven` without handing over an operation registry.
|
|
38
|
+
*/
|
|
39
|
+
import type { EntityCheckSuiteOptions } from './entity-check-plan.js';
|
|
40
|
+
/** Common to all three: who is claiming, and the prose a reviewer reads. */
|
|
41
|
+
interface ConformanceBase {
|
|
42
|
+
/** The name the suite registers under — `'meridian'`, `'engine-booking'`. */
|
|
43
|
+
readonly subject: string;
|
|
44
|
+
/**
|
|
45
|
+
* Why this claim is the right one, in the author's own words.
|
|
46
|
+
*
|
|
47
|
+
* Required on the two node-only kinds and optional on `driven`, for the same
|
|
48
|
+
* reason `alsoGrant.because` is required: an assessment with no reasoning is
|
|
49
|
+
* indistinguishable from nobody having thought about it. The emitter prints it
|
|
50
|
+
* verbatim, so it is written for a reader outside the repo.
|
|
51
|
+
*/
|
|
52
|
+
readonly because?: string;
|
|
53
|
+
}
|
|
54
|
+
/** A package whose declared entity checks are driven by the conformance kit. */
|
|
55
|
+
export interface DrivenConformance extends ConformanceBase, EntityCheckSuiteOptions {
|
|
56
|
+
readonly kind: 'driven';
|
|
57
|
+
/** The declared operation set — the same object the suite and the host read. */
|
|
58
|
+
readonly operations: Readonly<Record<string, object>>;
|
|
59
|
+
}
|
|
60
|
+
/** A package with a declared operation set that narrows nowhere. */
|
|
61
|
+
export interface DeclaredNodeOnlyConformance extends ConformanceBase {
|
|
62
|
+
readonly kind: 'declared';
|
|
63
|
+
readonly operations: Readonly<Record<string, object>>;
|
|
64
|
+
readonly because: string;
|
|
65
|
+
}
|
|
66
|
+
/** A package with no declared operation set, claiming node-only lexically. */
|
|
67
|
+
export interface AssertedNodeOnlyConformance extends ConformanceBase {
|
|
68
|
+
readonly kind: 'asserted';
|
|
69
|
+
/** Absolute paths to the module source the claim covers. */
|
|
70
|
+
readonly sources: readonly string[];
|
|
71
|
+
readonly because: string;
|
|
72
|
+
}
|
|
73
|
+
export type ConformanceDeclaration = DrivenConformance | DeclaredNodeOnlyConformance | AssertedNodeOnlyConformance;
|
|
74
|
+
/**
|
|
75
|
+
* Declare a driven surface. The result IS an `EntityCheckSuiteOptions`, so the
|
|
76
|
+
* test passes this same object straight through as the suite's options — there
|
|
77
|
+
* is no second place for `inputs` or `uncovered` to live and drift.
|
|
78
|
+
*/
|
|
79
|
+
export declare function declareEntityChecks(d: Omit<DrivenConformance, 'kind'>): DrivenConformance;
|
|
80
|
+
/** Declare a node-only surface whose emptiness the DECLARATION establishes. */
|
|
81
|
+
export declare function declareNodeOnly(d: Omit<DeclaredNodeOnlyConformance, 'kind'>): DeclaredNodeOnlyConformance;
|
|
82
|
+
/** Declare a node-only surface established only by a tripwire over source. */
|
|
83
|
+
export declare function assertNodeOnly(d: Omit<AssertedNodeOnlyConformance, 'kind'>): AssertedNodeOnlyConformance;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,4EAA4E;AAC5E,UAAU,eAAe;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,gFAAgF;AAChF,MAAM,WAAW,iBAAkB,SAAQ,eAAe,EAAE,uBAAuB;IACjF,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACvD;AAED,oEAAoE;AACpE,MAAM,WAAW,2BAA4B,SAAQ,eAAe;IAClE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,2BAA4B,SAAQ,eAAe;IAClE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,2BAA2B,GAC3B,2BAA2B,CAAC;AAEhC;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GACjC,iBAAiB,CAEnB;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAC7B,CAAC,EAAE,IAAI,CAAC,2BAA2B,EAAE,MAAM,CAAC,GAC3C,2BAA2B,CAE7B;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAC5B,CAAC,EAAE,IAAI,CAAC,2BAA2B,EAAE,MAAM,CAAC,GAC3C,2BAA2B,CAE7B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declare a driven surface. The result IS an `EntityCheckSuiteOptions`, so the
|
|
3
|
+
* test passes this same object straight through as the suite's options — there
|
|
4
|
+
* is no second place for `inputs` or `uncovered` to live and drift.
|
|
5
|
+
*/
|
|
6
|
+
export function declareEntityChecks(d) {
|
|
7
|
+
return { ...d, kind: 'driven' };
|
|
8
|
+
}
|
|
9
|
+
/** Declare a node-only surface whose emptiness the DECLARATION establishes. */
|
|
10
|
+
export function declareNodeOnly(d) {
|
|
11
|
+
return { ...d, kind: 'declared' };
|
|
12
|
+
}
|
|
13
|
+
/** Declare a node-only surface established only by a tripwire over source. */
|
|
14
|
+
export function assertNodeOnly(d) {
|
|
15
|
+
return { ...d, kind: 'asserted' };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAkFA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,CAAkC;IAElC,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAClC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAC7B,CAA4C;IAE5C,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACpC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAC5B,CAA4C;IAE5C,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of the conformance kit: what CAN be driven, and what cannot.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `entity-check-suite.ts` so the classification is reachable
|
|
5
|
+
* without a test runner (#866). The suite next door imports `vitest` at module
|
|
6
|
+
* load, which makes it unimportable outside vitest — and the trust-page emitter
|
|
7
|
+
* (`tools/conformance-emit.mts`) needs exactly this partition and none of the
|
|
8
|
+
* assertions. The suite's own note already said why this belongs on its own:
|
|
9
|
+
* *"Exported and pure so the classification is testable on its own. It is the
|
|
10
|
+
* part that decides what counts as covered, and a bug here is invisible in the
|
|
11
|
+
* worst way."*
|
|
12
|
+
*
|
|
13
|
+
* Nothing here imports a runner, a host or a filesystem. It reads a declared
|
|
14
|
+
* operation set and returns two lists.
|
|
15
|
+
*/
|
|
16
|
+
import type { EntityRef } from '@substrat-run/contracts';
|
|
17
|
+
/** What a vertical supplies so the kit can drive its operations. */
|
|
18
|
+
export interface EntityCheckFixture {
|
|
19
|
+
/**
|
|
20
|
+
* Create one entity of this declared type and return its id.
|
|
21
|
+
*
|
|
22
|
+
* Called for each case, so every case gets a world nobody else has touched —
|
|
23
|
+
* the operation under test may well delete the thing it is given.
|
|
24
|
+
*/
|
|
25
|
+
createEntity(entityType: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Grant `permission` to the probe principal, narrowed to exactly this entity.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately NOT the vertical's own sharing operation: using `share-list` to
|
|
30
|
+
* set up the test for `share-list` would prove only that it agrees with
|
|
31
|
+
* itself. Reach for the admin grant.
|
|
32
|
+
*/
|
|
33
|
+
grantOnEntity(permission: string, entity: EntityRef): Promise<void>;
|
|
34
|
+
/** Invoke as the probe principal — the one holding only narrowed grants. */
|
|
35
|
+
invoke(operation: string, input: Record<string, unknown>): Promise<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Is this error a permission denial? Defaults to `PermissionDenied`, which is
|
|
38
|
+
* what a stub throws; a fixture driving HTTP would test for its 403 instead.
|
|
39
|
+
*/
|
|
40
|
+
isDenial?(error: unknown): boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface EntityCheckSuiteOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Extra input fields per operation, beyond the entity id the kit supplies.
|
|
45
|
+
*
|
|
46
|
+
* Only needed where the operation's schema has REQUIRED fields besides the id
|
|
47
|
+
* — the kit reads the schema to find out, so an operation taking nothing but
|
|
48
|
+
* an id needs no entry here.
|
|
49
|
+
*/
|
|
50
|
+
readonly inputs?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
51
|
+
/**
|
|
52
|
+
* Permissions an operation needs BEYOND the one it declares, granted on the
|
|
53
|
+
* same target entity — with the reason it needs them.
|
|
54
|
+
*
|
|
55
|
+
* The first vertical this kit ran against produced one immediately.
|
|
56
|
+
* `todo/share-list` declares `list:manage` and honours it, then calls
|
|
57
|
+
* `ctx.grant` to hand `list:contribute` to the invitee — and delegation only
|
|
58
|
+
* works for a permission the caller HOLDS. A principal granted `list:manage`
|
|
59
|
+
* alone is refused, correctly, by the second gate.
|
|
60
|
+
*
|
|
61
|
+
* So an operation's declared permission is the gate it opens with, not
|
|
62
|
+
* necessarily the whole authority it exercises. That gap is invisible in
|
|
63
|
+
* production here only because todo's bootstrap grant hands every owner both
|
|
64
|
+
* keys on their own entity, so nobody ever holds one without the other.
|
|
65
|
+
*
|
|
66
|
+
* `because` is required rather than a comment: this is the one place the gap
|
|
67
|
+
* gets written down, and an entry without a reason is indistinguishable from
|
|
68
|
+
* someone widening the grant until the test went green.
|
|
69
|
+
*/
|
|
70
|
+
readonly alsoGrant?: Readonly<Record<string, {
|
|
71
|
+
readonly permissions: readonly string[];
|
|
72
|
+
readonly because: string;
|
|
73
|
+
}>>;
|
|
74
|
+
/**
|
|
75
|
+
* The in-scope operations this kit cannot generate, each with its reason.
|
|
76
|
+
*
|
|
77
|
+
* Asserted EXACTLY: an operation that becomes uncoverable, or one that stops
|
|
78
|
+
* being, fails until this list is updated. That is the point — it is the
|
|
79
|
+
* coverage gap made reviewable rather than invisible.
|
|
80
|
+
*/
|
|
81
|
+
readonly uncovered?: Readonly<Record<string, string>>;
|
|
82
|
+
/**
|
|
83
|
+
* The entity type to drive a `refFrom` check with (#896).
|
|
84
|
+
*
|
|
85
|
+
* An engine narrowing to a ref the caller supplies whole has no type of its own
|
|
86
|
+
* to name — that is the shape, not a gap in it. So the HARNESS names one and
|
|
87
|
+
* `createEntity` makes it. Which type is deliberately not the engine's
|
|
88
|
+
* business: an engine promising to honour whatever noun it is handed should not
|
|
89
|
+
* care which one a test picked, and if it does care, that is the finding.
|
|
90
|
+
*
|
|
91
|
+
* Needed only where the operation set holds a `refFrom` check. Without it those
|
|
92
|
+
* operations are reported as uncovered — never silently skipped.
|
|
93
|
+
*/
|
|
94
|
+
readonly refEntityType?: string;
|
|
95
|
+
}
|
|
96
|
+
/** One operation the kit can drive, with the declaration it was read from. */
|
|
97
|
+
export interface PlannedCheck {
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly key: string;
|
|
100
|
+
readonly entity: string;
|
|
101
|
+
/**
|
|
102
|
+
* Where the kit writes the target into the input.
|
|
103
|
+
*
|
|
104
|
+
* `{ kind: 'id' }` writes the bare id at `path` — the `idFrom` case. `{ kind:
|
|
105
|
+
* 'ref' }` writes the whole `{ entityType, entityId }` there instead, which is
|
|
106
|
+
* the `refFrom` case (#896); `path` may then be two segments, for a ref that
|
|
107
|
+
* travels inside a larger object.
|
|
108
|
+
*/
|
|
109
|
+
readonly target: {
|
|
110
|
+
readonly kind: 'id' | 'ref';
|
|
111
|
+
readonly path: readonly string[];
|
|
112
|
+
};
|
|
113
|
+
/** Input fields the schema fixes to one value, supplied by the kit (#890). */
|
|
114
|
+
readonly fixed: Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Partition an operation set into what this kit can drive and what it cannot.
|
|
118
|
+
*
|
|
119
|
+
* Exported and pure so the classification is testable on its own. It is the part
|
|
120
|
+
* that decides what counts as covered, and a bug here is invisible in the worst
|
|
121
|
+
* way — it would drop an operation from the suite while every remaining test
|
|
122
|
+
* still passed.
|
|
123
|
+
*
|
|
124
|
+
* Out of scope entirely (neither covered nor uncovered): an operation with a
|
|
125
|
+
* bare-key node check, or one declaring `narrows`. Neither claims an entity
|
|
126
|
+
* check, so neither has one to honour.
|
|
127
|
+
*
|
|
128
|
+
* An `entityFrom` operation appears ONCE PER ADMISSIBLE TYPE (#890) — the pair is
|
|
129
|
+
* what tells a correct check from a node check, and it is worth no less for the
|
|
130
|
+
* second type than for the first. So `callout/timeline` is driven twice, over a
|
|
131
|
+
* work order and over a protocol, and a handler that honoured the check for one
|
|
132
|
+
* and not the other has nowhere left to hide.
|
|
133
|
+
*/
|
|
134
|
+
export declare function planEntityCheckCoverage(operations: Readonly<Record<string, object>>, inputs?: Readonly<Record<string, Readonly<Record<string, unknown>>>>, refEntityType?: string): {
|
|
135
|
+
covered: PlannedCheck[];
|
|
136
|
+
uncovered: Record<string, string>;
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=entity-check-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-check-plan.d.ts","sourceRoot":"","sources":["../src/entity-check-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;;OAMG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,4EAA4E;IAC5E,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E;;;OAGG;IACH,QAAQ,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAC3B,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CACtF,CAAC;IACF;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AA8FD,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;IACnF,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC5C,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAM,EACxE,aAAa,CAAC,EAAE,MAAM,GACrB;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA4FhE"}
|