@lovrozagar/oat 0.9.1 → 0.10.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/README.md +103 -32
- package/dist/cli.js +56 -14
- package/dist/config/define-config.d.ts +7 -1
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/config/define-config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/report/console.d.ts +2 -1
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +33 -1
- package/dist/report/console.js.map +1 -1
- package/dist/report/matrix.d.ts +8 -13
- package/dist/report/matrix.d.ts.map +1 -1
- package/dist/report/matrix.js +21 -10
- package/dist/report/matrix.js.map +1 -1
- package/dist/report/render.d.ts +8 -0
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +94 -1
- package/dist/report/render.js.map +1 -1
- package/dist/runtime/checks.d.ts +42 -1
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +190 -24
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/finding.d.ts +14 -1
- package/dist/runtime/finding.d.ts.map +1 -1
- package/dist/runtime/finding.js +25 -6
- package/dist/runtime/finding.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +126 -66
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scope.d.ts +163 -0
- package/dist/runtime/scope.d.ts.map +1 -0
- package/dist/runtime/scope.js +423 -0
- package/dist/runtime/scope.js.map +1 -0
- package/dist/runtime/world.d.ts +7 -2
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +12 -6
- package/dist/runtime/world.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,8 @@ export interface Finding {
|
|
|
17
17
|
origin?: string;
|
|
18
18
|
/** Fixture filename when `uploads.each` drove the invocation. */
|
|
19
19
|
fixture?: string;
|
|
20
|
+
/** Operations this finding judges. Absent on run-level findings (teardown, network, pacing). */
|
|
21
|
+
operations?: string[];
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* A check that ran, could not reach a verdict, and stopped.
|
|
@@ -32,10 +34,21 @@ export interface Inconclusive {
|
|
|
32
34
|
entity: string;
|
|
33
35
|
/** Why no verdict was reachable, in the reader's terms. */
|
|
34
36
|
reason: string;
|
|
37
|
+
operations?: string[];
|
|
35
38
|
}
|
|
36
39
|
export declare class FindingCollector {
|
|
40
|
+
#private;
|
|
37
41
|
readonly findings: Finding[];
|
|
38
42
|
readonly inconclusive: Inconclusive[];
|
|
43
|
+
constructor(findings?: Finding[], inconclusive?: Inconclusive[], operations?: readonly string[]);
|
|
44
|
+
/**
|
|
45
|
+
* A view over the same findings that attributes everything it records to `operations`.
|
|
46
|
+
*
|
|
47
|
+
* Each check runs against its own view, so a finding lands on the operations the check judges
|
|
48
|
+
* without threading an id through every call site. Read-only checks run concurrently, so a
|
|
49
|
+
* shared "current check" would attribute one check's finding to another.
|
|
50
|
+
*/
|
|
51
|
+
attributed(operations: readonly string[]): FindingCollector;
|
|
39
52
|
/**
|
|
40
53
|
* Records that a check ran without reaching a verdict. Returns `undefined` so a check can
|
|
41
54
|
* `return ctx.findings.unresolved(...)` at the point it gives up, which keeps the reason
|
|
@@ -47,7 +60,7 @@ export declare class FindingCollector {
|
|
|
47
60
|
security(check: string, entity: string, summary: string, detail: string, evidence: Exchange[], fixture?: string): void;
|
|
48
61
|
spec(check: string, entity: string, summary: string, detail: string, evidence: Exchange[], fixture?: string): void;
|
|
49
62
|
gap(check: string, entity: string, summary: string, detail: string, fixture?: string): void;
|
|
50
|
-
blocked(check: string, entity: string, summary: string, cause: string): void;
|
|
63
|
+
blocked(check: string, entity: string, summary: string, cause: string, evidence?: Exchange[]): void;
|
|
51
64
|
checks(): string[];
|
|
52
65
|
byVerdict(verdict: Verdict): Finding[];
|
|
53
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,SAAS,CAAA;AAExG,MAAM,WAAW,OAAO;IACvB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,SAAS,CAAA;AAExG,MAAM,WAAW,OAAO;IACvB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAOD,qBAAa,gBAAgB;;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,CAAA;gBAGzB,QAAQ,GAAE,OAAO,EAAO,EAAE,YAAY,GAAE,YAAY,EAAO,EAAE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE;IAMvG;;;;;;OAMG;IACH,UAAU,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB;IAQ3D;;;;OAIG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS;IAKpE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9B,OAAO,CACN,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,QAAQ,CACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAI3F,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,QAAQ,EAAO,GAAG,IAAI;IAWvG,MAAM,IAAI,MAAM,EAAE;IAIlB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE;CAGtC"}
|
package/dist/runtime/finding.js
CHANGED
|
@@ -9,19 +9,38 @@ function withFixture(finding, fixture) {
|
|
|
9
9
|
return { ...finding, fixture };
|
|
10
10
|
}
|
|
11
11
|
export class FindingCollector {
|
|
12
|
-
findings
|
|
13
|
-
inconclusive
|
|
12
|
+
findings;
|
|
13
|
+
inconclusive;
|
|
14
|
+
#operations;
|
|
15
|
+
constructor(findings = [], inconclusive = [], operations) {
|
|
16
|
+
this.findings = findings;
|
|
17
|
+
this.inconclusive = inconclusive;
|
|
18
|
+
this.#operations = operations;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A view over the same findings that attributes everything it records to `operations`.
|
|
22
|
+
*
|
|
23
|
+
* Each check runs against its own view, so a finding lands on the operations the check judges
|
|
24
|
+
* without threading an id through every call site. Read-only checks run concurrently, so a
|
|
25
|
+
* shared "current check" would attribute one check's finding to another.
|
|
26
|
+
*/
|
|
27
|
+
attributed(operations) {
|
|
28
|
+
return new FindingCollector(this.findings, this.inconclusive, operations);
|
|
29
|
+
}
|
|
30
|
+
#stamp() {
|
|
31
|
+
return this.#operations === undefined ? {} : { operations: [...this.#operations] };
|
|
32
|
+
}
|
|
14
33
|
/**
|
|
15
34
|
* Records that a check ran without reaching a verdict. Returns `undefined` so a check can
|
|
16
35
|
* `return ctx.findings.unresolved(...)` at the point it gives up, which keeps the reason
|
|
17
36
|
* beside the condition that caused it rather than in a comment.
|
|
18
37
|
*/
|
|
19
38
|
unresolved(check, entity, reason) {
|
|
20
|
-
this.inconclusive.push({ check, entity, reason });
|
|
39
|
+
this.inconclusive.push({ check, entity, reason, ...this.#stamp() });
|
|
21
40
|
return undefined;
|
|
22
41
|
}
|
|
23
42
|
report(finding) {
|
|
24
|
-
this.findings.push(finding);
|
|
43
|
+
this.findings.push(finding.operations === undefined ? { ...finding, ...this.#stamp() } : finding);
|
|
25
44
|
}
|
|
26
45
|
backend(check, entity, summary, detail, evidence, fixture) {
|
|
27
46
|
this.report(withFixture({ check, detail, entity, evidence, summary, verdict: "BACKEND_BUG" }, fixture));
|
|
@@ -35,12 +54,12 @@ export class FindingCollector {
|
|
|
35
54
|
gap(check, entity, summary, detail, fixture) {
|
|
36
55
|
this.report(withFixture({ check, detail, entity, evidence: [], summary, verdict: "COVERAGE_GAP" }, fixture));
|
|
37
56
|
}
|
|
38
|
-
blocked(check, entity, summary, cause) {
|
|
57
|
+
blocked(check, entity, summary, cause, evidence = []) {
|
|
39
58
|
this.report({
|
|
40
59
|
check,
|
|
41
60
|
detail: `blocked by ${cause}`,
|
|
42
61
|
entity,
|
|
43
|
-
evidence
|
|
62
|
+
evidence,
|
|
44
63
|
summary,
|
|
45
64
|
verdict: "BLOCKED",
|
|
46
65
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/run.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type PersistedPrincipal } from "./principals.ts";
|
|
|
12
12
|
import { Client } from "./client.ts";
|
|
13
13
|
import type { ProgressHandler } from "./progress.ts";
|
|
14
14
|
import { type Finding, type Inconclusive } from "./finding.ts";
|
|
15
|
+
import { type ScopeReport } from "./scope.ts";
|
|
15
16
|
import { type TeardownReport } from "./teardown.ts";
|
|
16
17
|
export type PrincipalSpec = Principal;
|
|
17
18
|
export interface RunOptions {
|
|
@@ -26,7 +27,13 @@ export interface RunOptions {
|
|
|
26
27
|
seed?: number;
|
|
27
28
|
cohortSize?: number;
|
|
28
29
|
globalHeaders?: Record<string, string>;
|
|
30
|
+
/** Entity names; each expands into the target set. */
|
|
29
31
|
only?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Target operationIds (`*` globs, `<originId>:` prefixes). With `only`, the union is graded and
|
|
34
|
+
* every other operation is support — called, never graded. Omit both for a full run.
|
|
35
|
+
*/
|
|
36
|
+
ops?: string[];
|
|
30
37
|
/** Named profiles this run can select between. `"full"` and `"cheap"` exist without an entry. */
|
|
31
38
|
profiles?: Record<string, ProfileSpec>;
|
|
32
39
|
/** Active profile by name. Defaults to `"full"` — every operation runs, today's behaviour. */
|
|
@@ -86,6 +93,14 @@ export interface RunResult {
|
|
|
86
93
|
entity: string;
|
|
87
94
|
because: string;
|
|
88
95
|
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Checks a targeted run did not run on an entity because none of their subjects is a target.
|
|
98
|
+
* Neither run nor skipped: the check could apply, this run was not asked to grade it.
|
|
99
|
+
*/
|
|
100
|
+
checksOutOfScope: Array<{
|
|
101
|
+
check: string;
|
|
102
|
+
entity: string;
|
|
103
|
+
}>;
|
|
89
104
|
/** Checks that ran but could not reach a verdict — see `Inconclusive`. */
|
|
90
105
|
inconclusive: Inconclusive[];
|
|
91
106
|
/** Name of the profile that ran — `"full"` unless `--profile` / `config.profile` said otherwise. */
|
|
@@ -96,6 +111,8 @@ export interface RunResult {
|
|
|
96
111
|
operationId: string;
|
|
97
112
|
reason: string;
|
|
98
113
|
}>;
|
|
114
|
+
/** What this run graded, per operation — targets only when targeted, every operation when full. */
|
|
115
|
+
scope: ScopeReport;
|
|
99
116
|
created: number;
|
|
100
117
|
teardown: TeardownReport | null;
|
|
101
118
|
/** Credentials as they stood after acquire — written to `<outDir>/<datetime>/principals.json` by the CLI. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/runtime/run.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEpG,OAAO,KAAK,EACX,YAAY,EACZ,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/runtime/run.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/run.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { buildModel } from "../spec/graph.js";
|
|
9
9
|
import { dereference, loadSpec } from "../spec/load.js";
|
|
10
|
-
import { isAuthFlow } from "../config/define-config.js";
|
|
11
10
|
import { createPrincipal } from "./auth.js";
|
|
12
11
|
import { createExchangeJournal } from "./exchanges.js";
|
|
13
12
|
import { DEFAULT_NETWORK_RETRIES, DEFAULT_NETWORK_WAIT_MS, NetworkError, createNetworkGate, describeNetworkFailure, isNetworkError, probeOrigin, resolveRequestTimeoutMs, } from "./network.js";
|
|
@@ -19,12 +18,13 @@ import { FindingCollector } from "./finding.js";
|
|
|
19
18
|
import { reportFeatureGateSchemaDrift } from "./feature-gate.js";
|
|
20
19
|
import { formatUniqueSets } from "../spec/extensions.js";
|
|
21
20
|
import { excludedByProfile, resolveProfile } from "./profile.js";
|
|
21
|
+
import { authStepOperationIds, buildScopeReport, createIsAuthProvisioned, GradeLedger, resolveTargetScope, staticSubjects, } from "./scope.js";
|
|
22
22
|
import { buildRateLimitRules, RateLimiter } from "./rate-limit.js";
|
|
23
23
|
import { resolveEntityCapabilities } from "./query-capabilities.js";
|
|
24
24
|
import { Ledger } from "./teardown.js";
|
|
25
25
|
import { SchemaValidator } from "./validate.js";
|
|
26
26
|
import { isOverflowError, overflowFrom } from "./fixture.js";
|
|
27
|
-
import { SeedError, fillPath, listExisting, probeCreateFixtures,
|
|
27
|
+
import { SeedError, fillPath, listExisting, probeCreateFixtures, resolvePathScope, seedCohort, } from "./world.js";
|
|
28
28
|
function readPointer(body, pointer) {
|
|
29
29
|
const path = pointer
|
|
30
30
|
.replace(/^\$\.?/, "")
|
|
@@ -178,39 +178,6 @@ async function readExisting(listOp, client, headers, roots) {
|
|
|
178
178
|
const records = await listExisting(listOp, client, headers, scope);
|
|
179
179
|
return { records, scope };
|
|
180
180
|
}
|
|
181
|
-
/** operationIds named in any principal `auth.steps` — those creates already ran during login. */
|
|
182
|
-
function authStepOperationIds(principals) {
|
|
183
|
-
const ids = new Set();
|
|
184
|
-
for (const principal of principals) {
|
|
185
|
-
const auth = principal.auth;
|
|
186
|
-
if (auth === undefined || !isAuthFlow(auth))
|
|
187
|
-
continue;
|
|
188
|
-
for (const step of auth.steps) {
|
|
189
|
-
if ("operationId" in step)
|
|
190
|
-
ids.add(step.operationId);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return ids;
|
|
194
|
-
}
|
|
195
|
-
function createIsAuthProvisioned(createOp, authCreates) {
|
|
196
|
-
return createOp.freshPrincipal || authCreates.has(createOp.operationId);
|
|
197
|
-
}
|
|
198
|
-
function testableEntities(model, only, authCreates) {
|
|
199
|
-
return [...model.entities.values()]
|
|
200
|
-
.filter((entity) => {
|
|
201
|
-
if (only !== undefined && only.length > 0 && !only.includes(entity.name))
|
|
202
|
-
return false;
|
|
203
|
-
const createOp = entity.create === undefined ? undefined : model.byOperationId.get(entity.create);
|
|
204
|
-
const authProvisioned = createOp !== undefined && createIsAuthProvisioned(createOp, authCreates);
|
|
205
|
-
/* Register-as-create with no item route is the auth flow, not a fixture. */
|
|
206
|
-
if (authProvisioned && entity.read === undefined)
|
|
207
|
-
return false;
|
|
208
|
-
if (entity.invite !== null && entity.list !== undefined)
|
|
209
|
-
return true;
|
|
210
|
-
return entity.list !== undefined && entity.create !== undefined && entity.trackable;
|
|
211
|
-
})
|
|
212
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
213
|
-
}
|
|
214
181
|
export async function run(options) {
|
|
215
182
|
const startedAt = Date.now();
|
|
216
183
|
const findings = new FindingCollector();
|
|
@@ -394,6 +361,32 @@ export async function run(options) {
|
|
|
394
361
|
if (options.principals[0] === undefined)
|
|
395
362
|
throw new Error("oat: at least one principal is required");
|
|
396
363
|
const originClients = await loadOriginClients(options.origins ?? [], hooks, options.maxInFlight ?? 4, options.globalHeaders ?? {}, onExchange, httpHooks, networkClient);
|
|
364
|
+
/* Resolved before any principal signs up: a typo in --ops must cost nothing. */
|
|
365
|
+
const runScope = resolveTargetScope(model, {
|
|
366
|
+
...(options.ops === undefined ? {} : { ops: options.ops }),
|
|
367
|
+
...(options.only === undefined ? {} : { only: options.only }),
|
|
368
|
+
authCreates,
|
|
369
|
+
exclusion: { profile: profile.name, reason: (op) => excludedByProfile(op, profile.spec) },
|
|
370
|
+
origins: new Map([...originClients].map(([id, origin]) => [id, origin.model])),
|
|
371
|
+
});
|
|
372
|
+
const grades = new GradeLedger();
|
|
373
|
+
tick({
|
|
374
|
+
message: runScope.mode === "full"
|
|
375
|
+
? "scope: full"
|
|
376
|
+
: `scope: targeted · ${runScope.targets.size} operation(s) · ${runScope.entities.length} entit${runScope.entities.length === 1 ? "y" : "ies"}`,
|
|
377
|
+
phase: "load",
|
|
378
|
+
requests: client.transcript.length,
|
|
379
|
+
});
|
|
380
|
+
const scopeReport = () => buildScopeReport({
|
|
381
|
+
authCreates,
|
|
382
|
+
baseUrl: options.baseUrl,
|
|
383
|
+
findings: findings.findings,
|
|
384
|
+
inconclusive: findings.inconclusive,
|
|
385
|
+
ledger: grades,
|
|
386
|
+
model,
|
|
387
|
+
scope: runScope,
|
|
388
|
+
transcript: client.transcript,
|
|
389
|
+
});
|
|
397
390
|
const uploads = {
|
|
398
391
|
seed,
|
|
399
392
|
...(options.uploads === undefined ? {} : { uploads: options.uploads }),
|
|
@@ -413,6 +406,7 @@ export async function run(options) {
|
|
|
413
406
|
throw error;
|
|
414
407
|
noteNetwork(error, "auth");
|
|
415
408
|
return {
|
|
409
|
+
checksOutOfScope: [],
|
|
416
410
|
checksRun: [],
|
|
417
411
|
checksSkipped: [],
|
|
418
412
|
checksSuppressed: [],
|
|
@@ -425,6 +419,7 @@ export async function run(options) {
|
|
|
425
419
|
principals: [],
|
|
426
420
|
profile: profile.name,
|
|
427
421
|
profileExclusions,
|
|
422
|
+
scope: scopeReport(),
|
|
428
423
|
teardown: null,
|
|
429
424
|
...(journal === null ? {} : { exchanges: { count: journal.count } }),
|
|
430
425
|
...(networkOutcome === undefined ? {} : { network: networkOutcome }),
|
|
@@ -443,6 +438,11 @@ export async function run(options) {
|
|
|
443
438
|
const checksRun = new Set();
|
|
444
439
|
const checksSkipped = [];
|
|
445
440
|
const checksSuppressed = [];
|
|
441
|
+
const checksOutOfScope = [];
|
|
442
|
+
const profileExcludes = (operationId) => {
|
|
443
|
+
const op = model.byOperationId.get(operationId);
|
|
444
|
+
return op !== undefined && excludedByProfile(op, profile.spec) !== null;
|
|
445
|
+
};
|
|
446
446
|
const excludedIds = new Set();
|
|
447
447
|
const excludeOp = (entity, op) => {
|
|
448
448
|
if (op === undefined)
|
|
@@ -454,7 +454,9 @@ export async function run(options) {
|
|
|
454
454
|
return true;
|
|
455
455
|
excludedIds.add(op.operationId);
|
|
456
456
|
profileExclusions.push({ entity: entity.name, operationId: op.operationId, reason });
|
|
457
|
-
findings
|
|
457
|
+
findings
|
|
458
|
+
.attributed([op.operationId])
|
|
459
|
+
.gap("profile.skip", entity.name, `${op.operationId} excluded by profile "${profile.name}"`, `${reason}, under --profile ${profile.name}. Checks that depend on this operation stand ` +
|
|
458
460
|
"down rather than run against data oat did not create through it.");
|
|
459
461
|
return true;
|
|
460
462
|
};
|
|
@@ -465,6 +467,14 @@ export async function run(options) {
|
|
|
465
467
|
return;
|
|
466
468
|
if (networkGate.exhausted)
|
|
467
469
|
return;
|
|
470
|
+
/* Targets this entity's checks can grade. Seeding findings land on them: under --ops a seed
|
|
471
|
+
* that fails leaves exactly these ungraded, and the report has to say which. */
|
|
472
|
+
const entityTargets = runScope.mode === "full"
|
|
473
|
+
? createOp === undefined
|
|
474
|
+
? []
|
|
475
|
+
: [createOp.operationId]
|
|
476
|
+
: [...staticSubjects(entity, model)].filter((id) => runScope.inScope(id));
|
|
477
|
+
const entityFindings = entityTargets.length === 0 ? findings : findings.attributed(entityTargets);
|
|
468
478
|
const inviteOnly = entity.invite !== null && createOp === undefined;
|
|
469
479
|
const authProvisioned = createOp !== undefined && createIsAuthProvisioned(createOp, authCreates);
|
|
470
480
|
if (createOp === undefined && !inviteOnly)
|
|
@@ -501,7 +511,7 @@ export async function run(options) {
|
|
|
501
511
|
...alpha.roots,
|
|
502
512
|
});
|
|
503
513
|
if (existing.records.length === 0) {
|
|
504
|
-
|
|
514
|
+
entityFindings.blocked("profile.skip", entity.name, `could not test "${entity.name}"`, `create is excluded by profile "${profile.name}" and the list route returned no ` +
|
|
505
515
|
"existing records to fall back on.");
|
|
506
516
|
return;
|
|
507
517
|
}
|
|
@@ -522,7 +532,7 @@ export async function run(options) {
|
|
|
522
532
|
}
|
|
523
533
|
else {
|
|
524
534
|
try {
|
|
525
|
-
scope = await
|
|
535
|
+
scope = await resolvePathScope(createOp, model, client, {
|
|
526
536
|
authHeaders: alpha.headers,
|
|
527
537
|
roots: rootValues,
|
|
528
538
|
seed,
|
|
@@ -541,13 +551,13 @@ export async function run(options) {
|
|
|
541
551
|
uploads,
|
|
542
552
|
}, scope);
|
|
543
553
|
if (cohort.uniqueGap !== undefined) {
|
|
544
|
-
|
|
554
|
+
entityFindings.gap("world.seed", entity.name, cohort.uniqueGap, cohort.uniqueGap);
|
|
545
555
|
}
|
|
546
556
|
if (cohort.adopted === true) {
|
|
547
557
|
/* Plan limit after an effect already created the row: keep the id so children
|
|
548
558
|
* (row after extract→table) can seed, but do not assert write-path oracles
|
|
549
559
|
* against a body oat never submitted. */
|
|
550
|
-
|
|
560
|
+
entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" hit a plan limit; using an existing same-tenant record`, `${createOp.operationId} returned a plan-limit refusal and the list already ` +
|
|
551
561
|
"had a record — likely an earlier x-effects create. Write-path checks stand " +
|
|
552
562
|
"down rather than treat payment_required as a backend defect.");
|
|
553
563
|
records = cohort.records;
|
|
@@ -567,7 +577,7 @@ export async function run(options) {
|
|
|
567
577
|
* but unique-conflict checks still run against that row. Write-path oracles
|
|
568
578
|
* that need a body oat submitted stay skipped. The seed 409 is not the
|
|
569
579
|
* unique check passing — that check is the explicit second POST. */
|
|
570
|
-
|
|
580
|
+
entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" could not insert because of x-unique`, `${createOp.operationId} returned 409 and the list already had a record. ` +
|
|
571
581
|
`x-unique: ${formatUniqueSets(createOp.unique)}. The create could not insert; ` +
|
|
572
582
|
"unique-conflict checks still run against that row. Write-path oracles that " +
|
|
573
583
|
"need a body oat submitted stand down.");
|
|
@@ -588,15 +598,15 @@ export async function run(options) {
|
|
|
588
598
|
/* Same degradation a profile-excluded create takes: the tag said this
|
|
589
599
|
* principal cannot create the row, so a correct 403 is coverage, not a
|
|
590
600
|
* seed defect. The 403 body still has to match the documented schema. */
|
|
591
|
-
reportFeatureGateSchemaDrift(
|
|
592
|
-
|
|
601
|
+
reportFeatureGateSchemaDrift(entityFindings, validator, createOp, model.rawOperations.get(createOp.operationId), cohort.featureGate.exchange, entity.name);
|
|
602
|
+
entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" is gated by ${cohort.featureGate.detail}`, `${cohort.featureGate.detail}. Checks that need a row oat created stand down ` +
|
|
593
603
|
"rather than treat the documented 403 as a defect.");
|
|
594
604
|
const existing = await readExisting(listOp, client, alpha.headers(), {
|
|
595
605
|
...options.roots,
|
|
596
606
|
...alpha.roots,
|
|
597
607
|
});
|
|
598
608
|
if (existing.records.length === 0) {
|
|
599
|
-
|
|
609
|
+
entityFindings.blocked("world.seed", entity.name, `could not test "${entity.name}"`, `${cohort.featureGate.detail} and the list route returned no existing ` + "records to fall back on.");
|
|
600
610
|
return;
|
|
601
611
|
}
|
|
602
612
|
scope = { created: [], values: existing.scope };
|
|
@@ -622,13 +632,13 @@ export async function run(options) {
|
|
|
622
632
|
}
|
|
623
633
|
if (isOverflowError(error)) {
|
|
624
634
|
const overflow = overflowFrom(error, createOp.operationId);
|
|
625
|
-
|
|
635
|
+
entityFindings.gap("world.seed", entity.name, overflow.message, overflow.message);
|
|
626
636
|
const existing = await readExisting(listOp, client, alpha.headers(), {
|
|
627
637
|
...options.roots,
|
|
628
638
|
...alpha.roots,
|
|
629
639
|
});
|
|
630
640
|
if (existing.records.length === 0) {
|
|
631
|
-
|
|
641
|
+
entityFindings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, overflow.message);
|
|
632
642
|
return;
|
|
633
643
|
}
|
|
634
644
|
scope = { created: [], values: existing.scope };
|
|
@@ -639,13 +649,23 @@ export async function run(options) {
|
|
|
639
649
|
const cause = error instanceof SeedError ? error.cause_ : "unknown";
|
|
640
650
|
const status = error instanceof SeedError ? error.status : undefined;
|
|
641
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
652
|
+
const failedOp = (error instanceof SeedError ? error.operationId : undefined) ?? createOp.operationId;
|
|
653
|
+
const evidence = error instanceof SeedError && error.exchange !== undefined
|
|
654
|
+
? [error.exchange]
|
|
655
|
+
: client.transcript.filter((e) => e.status >= 500).slice(-1);
|
|
656
|
+
/* Under --ops a create nobody targeted is support. Its failure leaves the targets
|
|
657
|
+
* ungraded — blocked, with the evidence — but it is not what this run was asked to
|
|
658
|
+
* judge, so it is not reported as a defect of its own. */
|
|
659
|
+
const supportFailed = runScope.mode === "targeted" && !runScope.inScope(failedOp);
|
|
642
660
|
/* A create that fails with 5xx is not a fixture problem — it is the defect.
|
|
643
661
|
* Reporting it as merely "blocked" buries the most serious thing oat found. */
|
|
644
|
-
if (status !== undefined && status >= 500) {
|
|
645
|
-
findings
|
|
662
|
+
if (status !== undefined && status >= 500 && !supportFailed) {
|
|
663
|
+
findings
|
|
664
|
+
.attributed([failedOp])
|
|
665
|
+
.backend("create.does-not-error", entity.name, `creating a "${entity.name}" fails with a server error`, `${message}. The request body was generated from the documented schema, so ` +
|
|
646
666
|
"either the handler rejects input the document permits, or it is failing " +
|
|
647
667
|
"outright. Everything downstream of this entity is untestable until it is " +
|
|
648
|
-
"fixed.",
|
|
668
|
+
"fixed.", evidence);
|
|
649
669
|
}
|
|
650
670
|
/* Fall back to whatever already exists. A backend whose create is broken can still
|
|
651
671
|
* have a working list, and read-only coverage beats no coverage — this is exactly
|
|
@@ -655,10 +675,10 @@ export async function run(options) {
|
|
|
655
675
|
...alpha.roots,
|
|
656
676
|
});
|
|
657
677
|
if (existing.records.length === 0) {
|
|
658
|
-
|
|
678
|
+
entityFindings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, supportFailed ? `support operation ${failedOp} failing: ${message}` : `${cause}: ${message}`, evidence);
|
|
659
679
|
return;
|
|
660
680
|
}
|
|
661
|
-
|
|
681
|
+
entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" failed; running read-only checks against existing records`, `${cause}: ${message}. Write-path and lifecycle checks are skipped for this entity.`);
|
|
662
682
|
scope = { created: [], values: existing.scope };
|
|
663
683
|
records = existing.records;
|
|
664
684
|
degraded = true;
|
|
@@ -668,7 +688,7 @@ export async function run(options) {
|
|
|
668
688
|
const actorOf = async (principal, seedOffset) => {
|
|
669
689
|
const roots = { ...options.roots, ...principal.roots };
|
|
670
690
|
try {
|
|
671
|
-
const next = await
|
|
691
|
+
const next = await resolvePathScope(listOp, model, client, {
|
|
672
692
|
authHeaders: principal.headers,
|
|
673
693
|
roots,
|
|
674
694
|
seed: seed + seedOffset,
|
|
@@ -786,9 +806,12 @@ export async function run(options) {
|
|
|
786
806
|
actors,
|
|
787
807
|
altAuth,
|
|
788
808
|
altScope,
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
809
|
+
/* Narrowed to targets: these are the subjects their checks invoke, and invoking an
|
|
810
|
+
* untargeted one is exactly the cost --ops exists to skip. */
|
|
811
|
+
asyncOps: model.operations.filter((op) => op.entity === entity.name && op.async !== null && invocable(op) && runScope.inScope(op.operationId)),
|
|
812
|
+
effectOps: model.operations.filter((op) => op.entity === entity.name && op.effects.length > 0 && invocable(op) && runScope.inScope(op.operationId)),
|
|
813
|
+
waitOps: model.operations.filter((op) => op.entity === entity.name && op.wait !== null && invocable(op) && runScope.inScope(op.operationId)),
|
|
814
|
+
inScope: (op) => runScope.inScope(op.operationId),
|
|
792
815
|
hooks,
|
|
793
816
|
outOfBand,
|
|
794
817
|
auth: alpha.headers,
|
|
@@ -833,7 +856,20 @@ export async function run(options) {
|
|
|
833
856
|
: undefined,
|
|
834
857
|
};
|
|
835
858
|
entitiesTested.push(entity.name);
|
|
859
|
+
/* Subjects each check grades on this entity: in scope, and not excluded by the profile. */
|
|
860
|
+
const gradedBy = new Map();
|
|
836
861
|
const runOne = async (check) => {
|
|
862
|
+
const graded = gradedBy.get(check.id) ?? [];
|
|
863
|
+
let judged;
|
|
864
|
+
const view = {
|
|
865
|
+
...ctx,
|
|
866
|
+
...(graded.length === 0 ? {} : { findings: findings.attributed(graded) }),
|
|
867
|
+
judged: (operationIds) => {
|
|
868
|
+
judged ??= new Set();
|
|
869
|
+
for (const id of operationIds)
|
|
870
|
+
judged.add(id);
|
|
871
|
+
},
|
|
872
|
+
};
|
|
837
873
|
checksRun.add(check.id);
|
|
838
874
|
currentCheck = check.id;
|
|
839
875
|
currentPhase = "test";
|
|
@@ -846,15 +882,16 @@ export async function run(options) {
|
|
|
846
882
|
requests: client.transcript.length,
|
|
847
883
|
});
|
|
848
884
|
try {
|
|
849
|
-
await check.run(
|
|
885
|
+
await check.run(view);
|
|
850
886
|
}
|
|
851
887
|
catch (error) {
|
|
852
888
|
if (isNetworkError(error)) {
|
|
853
889
|
noteNetwork(error, entity.name);
|
|
854
890
|
return;
|
|
855
891
|
}
|
|
856
|
-
findings.gap(check.id, entity.name, `check "${check.id}" could not complete`, error instanceof Error ? error.message : String(error));
|
|
892
|
+
view.findings.gap(check.id, entity.name, `check "${check.id}" could not complete`, error instanceof Error ? error.message : String(error));
|
|
857
893
|
}
|
|
894
|
+
grades.graded(judged === undefined ? graded : graded.filter((id) => judged?.has(id) === true), check.id);
|
|
858
895
|
};
|
|
859
896
|
/* Cascade suppression: a check whose premise is already known broken would report a
|
|
860
897
|
* consequence, not a defect. One root cause, one finding. */
|
|
@@ -881,6 +918,7 @@ export async function run(options) {
|
|
|
881
918
|
const because = failed ? dependency : inherited;
|
|
882
919
|
suppressedBy.set(check.id, because);
|
|
883
920
|
checksSuppressed.push({ because, check: check.id, entity: entity.name });
|
|
921
|
+
grades.suppress(gradedBy.get(check.id) ?? [], check.id);
|
|
884
922
|
return true;
|
|
885
923
|
}
|
|
886
924
|
return false;
|
|
@@ -902,14 +940,19 @@ export async function run(options) {
|
|
|
902
940
|
await Promise.all(pending.map(runOne));
|
|
903
941
|
};
|
|
904
942
|
for (const check of CHECKS) {
|
|
943
|
+
const graded = check.subjects(entity, model).filter((id) => runScope.inScope(id) && !profileExcludes(id));
|
|
944
|
+
/* Out of scope is not a skip: the check could have run, this run was not asked to. */
|
|
945
|
+
if (runScope.mode === "targeted" && graded.length === 0) {
|
|
946
|
+
checksOutOfScope.push({ check: check.id, entity: entity.name });
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
gradedBy.set(check.id, graded);
|
|
905
950
|
if (!check.applicable(ctx)) {
|
|
906
951
|
/* Recorded, not dropped: on an API shaped unlike the fixture this is most of the
|
|
907
952
|
* suite, and a silent skip reads exactly like a clean result. */
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
needs: check.needs ?? "an unstated precondition",
|
|
912
|
-
});
|
|
953
|
+
const needs = check.needs ?? "an unstated precondition";
|
|
954
|
+
checksSkipped.push({ check: check.id, entity: entity.name, needs });
|
|
955
|
+
grades.skipped(graded, needs);
|
|
913
956
|
continue;
|
|
914
957
|
}
|
|
915
958
|
if (check.mutates === true) {
|
|
@@ -935,7 +978,7 @@ export async function run(options) {
|
|
|
935
978
|
* entities: a child create in flight while a parent page-walk runs invents pagination
|
|
936
979
|
* findings, so there are no entity lanes.
|
|
937
980
|
*/
|
|
938
|
-
const queue =
|
|
981
|
+
const queue = runScope.entities;
|
|
939
982
|
entityTotal = queue.length;
|
|
940
983
|
for (const [index, entity] of queue.entries()) {
|
|
941
984
|
if (networkGate.exhausted) {
|
|
@@ -982,8 +1025,9 @@ export async function run(options) {
|
|
|
982
1025
|
rank: principal.rank,
|
|
983
1026
|
...(principal.inviteAs === undefined ? {} : { inviteAs: principal.inviteAs }),
|
|
984
1027
|
}));
|
|
1028
|
+
const coverage = scopeReport();
|
|
985
1029
|
if ((options.origins ?? []).length > 0 && options.skipPrincipalTeardown !== true) {
|
|
986
|
-
await runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, entitiesTested);
|
|
1030
|
+
await runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, checksOutOfScope, entitiesTested, runScope, coverage);
|
|
987
1031
|
}
|
|
988
1032
|
if (options.skipPrincipalTeardown !== true) {
|
|
989
1033
|
await teardownPrincipals(resolved, hooks, findings);
|
|
@@ -994,6 +1038,7 @@ export async function run(options) {
|
|
|
994
1038
|
requests: client.transcript.length,
|
|
995
1039
|
});
|
|
996
1040
|
return {
|
|
1041
|
+
checksOutOfScope,
|
|
997
1042
|
checksRun: [...checksRun].sort(),
|
|
998
1043
|
checksSkipped,
|
|
999
1044
|
checksSuppressed,
|
|
@@ -1006,6 +1051,7 @@ export async function run(options) {
|
|
|
1006
1051
|
principals: persisted,
|
|
1007
1052
|
profile: profile.name,
|
|
1008
1053
|
profileExclusions,
|
|
1054
|
+
scope: coverage,
|
|
1009
1055
|
teardown,
|
|
1010
1056
|
...(journal === null ? {} : { exchanges: { count: journal.count } }),
|
|
1011
1057
|
...(networkOutcome === undefined ? {} : { network: networkOutcome }),
|
|
@@ -1028,8 +1074,15 @@ async function loadOriginClients(origins, hooks, maxInFlight, globalHeaders, onE
|
|
|
1028
1074
|
}
|
|
1029
1075
|
return map;
|
|
1030
1076
|
}
|
|
1031
|
-
async function runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, entitiesTested) {
|
|
1077
|
+
async function runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, checksOutOfScope, entitiesTested, runScope, coverage) {
|
|
1032
1078
|
for (const origin of options.origins ?? []) {
|
|
1079
|
+
/* Targets were resolved against every origin up front; an origin none of them name has
|
|
1080
|
+
* nothing to grade, and running it anyway is the cost --ops exists to skip. */
|
|
1081
|
+
const originOps = runScope.origins.get(origin.id);
|
|
1082
|
+
if (runScope.mode === "targeted" && originOps === undefined) {
|
|
1083
|
+
coverage.originsSkipped.push(origin.id);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1033
1086
|
const originHooks = options.hooks === undefined
|
|
1034
1087
|
? undefined
|
|
1035
1088
|
: (() => {
|
|
@@ -1048,7 +1101,7 @@ async function runSecondaryOrigins(options, persisted, findings, checksRun, chec
|
|
|
1048
1101
|
...(options.roots === undefined ? {} : { roots: options.roots }),
|
|
1049
1102
|
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
1050
1103
|
...(options.cohortSize === undefined ? {} : { cohortSize: options.cohortSize }),
|
|
1051
|
-
...(
|
|
1104
|
+
...(originOps === undefined ? {} : { ops: originOps }),
|
|
1052
1105
|
...(options.profiles === undefined ? {} : { profiles: options.profiles }),
|
|
1053
1106
|
...(options.profile === undefined ? {} : { profile: options.profile }),
|
|
1054
1107
|
...(options.rateLimits === undefined ? {} : { rateLimits: options.rateLimits }),
|
|
@@ -1073,6 +1126,13 @@ async function runSecondaryOrigins(options, persisted, findings, checksRun, chec
|
|
|
1073
1126
|
}
|
|
1074
1127
|
for (const entity of result.entitiesTested)
|
|
1075
1128
|
entitiesTested.push(`${origin.id}:${entity}`);
|
|
1129
|
+
for (const item of result.checksOutOfScope) {
|
|
1130
|
+
checksOutOfScope.push({ ...item, entity: `${origin.id}:${item.entity}` });
|
|
1131
|
+
}
|
|
1132
|
+
for (const op of result.scope.operations)
|
|
1133
|
+
coverage.operations.push({ ...op, origin: origin.id });
|
|
1134
|
+
for (const use of result.scope.support)
|
|
1135
|
+
coverage.support.push({ ...use, origin: origin.id });
|
|
1076
1136
|
for (const item of result.inconclusive) {
|
|
1077
1137
|
findings.unresolved(item.check, `${origin.id}:${item.entity}`, item.reason);
|
|
1078
1138
|
}
|