@am_shork/attest 0.5.0 → 0.7.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/CHANGELOG.md +1063 -143
- package/README.md +1 -1
- package/dist/cli/index.js +13 -11
- package/dist/cli/json.js +6 -1
- package/dist/cli/report.d.ts +18 -0
- package/dist/cli/report.js +41 -0
- package/dist/core/compiler.d.ts +40 -0
- package/dist/core/compiler.js +64 -0
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +6 -0
- package/dist/core/gate.d.ts +48 -2
- package/dist/core/gate.js +73 -14
- package/dist/core/loader.js +13 -0
- package/dist/core/locate.d.ts +62 -0
- package/dist/core/locate.js +94 -11
- package/dist/core/merge.d.ts +54 -0
- package/dist/core/merge.js +257 -0
- package/dist/core/pipeline.d.ts +13 -0
- package/dist/core/pipeline.js +319 -40
- package/dist/core/render.js +129 -14
- package/dist/core/req-suite.d.ts +5 -0
- package/dist/core/req-suite.js +27 -0
- package/dist/core/runner.js +26 -8
- package/dist/core/skill.js +52 -12
- package/dist/core/splice.d.ts +52 -0
- package/dist/core/splice.js +189 -0
- package/dist/core/static-registry.d.ts +30 -0
- package/dist/core/static-registry.js +36 -0
- package/dist/core/status.js +21 -3
- package/dist/core/terminal.d.ts +23 -0
- package/dist/core/terminal.js +25 -9
- package/dist/core/validator.d.ts +5 -1
- package/dist/core/validator.js +23 -2
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.js +43 -15
- package/package.json +1 -1
|
@@ -39,4 +39,34 @@ export type DeltaReadResult = {
|
|
|
39
39
|
* (write the value inline, or `--eval`) is the same sentence either way.
|
|
40
40
|
*/
|
|
41
41
|
export declare function readDeltaSource(file: string, source: string): DeltaReadResult;
|
|
42
|
+
/**
|
|
43
|
+
* Where a new entry may be written into a registry file's literal, as an offset
|
|
44
|
+
* into its source (design §7).
|
|
45
|
+
*
|
|
46
|
+
* The insertion point rather than a rewritten file, because `--apply` must not
|
|
47
|
+
* regenerate a `*.reqs.ts`. Every registry this tool merges into is hand-written
|
|
48
|
+
* and hand-commented, and rendering one back out of a `Registry` object would
|
|
49
|
+
* silently drop every comment and every choice of layout in it — the
|
|
50
|
+
* "destructive on a file the user cannot regenerate" shape the `AGENTS.md`
|
|
51
|
+
* proposal was rejected for. An offset lets the splice be a pure insertion: every
|
|
52
|
+
* other byte of the file is the byte that was there before, which is a property
|
|
53
|
+
* that can be stated and tested rather than hoped for.
|
|
54
|
+
*
|
|
55
|
+
* Lives here because this is the module that already knows how to find the
|
|
56
|
+
* literal, and the one place `typescript` is imported for that job. A second
|
|
57
|
+
* walker would be a second answer to "where does this registry's body end".
|
|
58
|
+
*
|
|
59
|
+
* `undefined` when the file is not a registry this tool can read — the same
|
|
60
|
+
* condition `readRegistrySource` reports as `registry-not-static` or
|
|
61
|
+
* `registry-no-default`, and the caller has already run that check.
|
|
62
|
+
*/
|
|
63
|
+
export interface RegistryInsertion {
|
|
64
|
+
/** Offset to insert at. Everything before and after it is preserved. */
|
|
65
|
+
offset: number;
|
|
66
|
+
/** The indentation the file's existing entries use, reproduced for new ones. */
|
|
67
|
+
indent: string;
|
|
68
|
+
/** Whether the insertion has to open with a `,` — false only for an empty registry. */
|
|
69
|
+
leadingComma: boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare function registryInsertionPoint(file: string, source: string): RegistryInsertion | undefined;
|
|
42
72
|
//# sourceMappingURL=static-registry.d.ts.map
|
|
@@ -322,4 +322,40 @@ function numericValue(node) {
|
|
|
322
322
|
throw new NotStatic(node, 'a number this reader cannot spell out');
|
|
323
323
|
return value;
|
|
324
324
|
}
|
|
325
|
+
export function registryInsertionPoint(file, source) {
|
|
326
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
327
|
+
const exported = defaultExportExpression(sf);
|
|
328
|
+
if (!exported)
|
|
329
|
+
return undefined;
|
|
330
|
+
const arg = authoringCall(exported, sf, 'defineRequirements');
|
|
331
|
+
if (!arg || !ts.isObjectLiteralExpression(arg))
|
|
332
|
+
return undefined;
|
|
333
|
+
const last = arg.properties[arg.properties.length - 1];
|
|
334
|
+
if (!last) {
|
|
335
|
+
// An empty registry: open the body rather than continue it. The trailing
|
|
336
|
+
// newline is written by the caller, so `{` does not end up sharing a line
|
|
337
|
+
// with the entry and the closing `}`.
|
|
338
|
+
return { offset: arg.getStart(sf) + 1, indent: indentOf(sf, source, arg) + ' ', leadingComma: false };
|
|
339
|
+
}
|
|
340
|
+
// Deliberately the *end of the last property*, not the end of the literal:
|
|
341
|
+
// inserting here sits before any trailing comma the file already has, so one
|
|
342
|
+
// leading comma is correct whether or not that comma is present, and the
|
|
343
|
+
// file's own trailing-comma style is left exactly as it was.
|
|
344
|
+
return { offset: last.getEnd(), indent: indentOf(sf, source, last), leadingComma: true };
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The whitespace a node's line opens with.
|
|
348
|
+
*
|
|
349
|
+
* Read out of the source rather than computed from the column, so a file
|
|
350
|
+
* indented with tabs is continued with tabs. Anything else on the line (a
|
|
351
|
+
* property sharing a line with another) falls back to two spaces, which is the
|
|
352
|
+
* only case where this guesses.
|
|
353
|
+
*/
|
|
354
|
+
function indentOf(sf, source, node) {
|
|
355
|
+
const start = node.getStart(sf);
|
|
356
|
+
const { line } = sf.getLineAndCharacterOfPosition(start);
|
|
357
|
+
const lineStart = sf.getPositionOfLineAndCharacter(line, 0);
|
|
358
|
+
const prefix = source.slice(lineStart, start);
|
|
359
|
+
return /^[\t ]*$/.test(prefix) ? prefix : ' ';
|
|
360
|
+
}
|
|
325
361
|
//# sourceMappingURL=static-registry.js.map
|
package/dist/core/status.js
CHANGED
|
@@ -29,10 +29,28 @@ import { hasRecordedRed, recordedOutcome } from './red-record.js';
|
|
|
29
29
|
* author reads them in.
|
|
30
30
|
*/
|
|
31
31
|
export function statusRows(addedIds, plan, firstRun) {
|
|
32
|
+
// Index the scenarios once, the way `validator.ts`'s `detectPotentialDrift`
|
|
33
|
+
// does and for the reason recorded there: filtering the whole plan per
|
|
34
|
+
// requirement is O(requirements x scenarios). The plan here is the *merged*
|
|
35
|
+
// one, so the inner term is the whole repository's scenario count while the
|
|
36
|
+
// outer is only what this change adds.
|
|
37
|
+
//
|
|
38
|
+
// The cost is invisible today — 20 added ids against 2000 scenarios is 40k
|
|
39
|
+
// comparisons, well under a millisecond — so this is filed as the
|
|
40
|
+
// inconsistency it is rather than as a slow path. Two functions over one
|
|
41
|
+
// shape held two beliefs about whether it is worth indexing; they hold one
|
|
42
|
+
// now. Insertion order is plan order, which is file then line, so the
|
|
43
|
+
// scenarios a row carries are still in the order their author reads them.
|
|
44
|
+
const byReqId = new Map();
|
|
45
|
+
for (const s of plan.scenarios) {
|
|
46
|
+
const group = byReqId.get(s.reqId);
|
|
47
|
+
if (group)
|
|
48
|
+
group.push(s);
|
|
49
|
+
else
|
|
50
|
+
byReqId.set(s.reqId, [s]);
|
|
51
|
+
}
|
|
32
52
|
return [...addedIds].sort(byCodeUnit).map((reqId) => {
|
|
33
|
-
const scenarios =
|
|
34
|
-
.filter((s) => s.reqId === reqId)
|
|
35
|
-
.map((s) => ({
|
|
53
|
+
const scenarios = (byReqId.get(reqId) ?? []).map((s) => ({
|
|
36
54
|
name: s.name,
|
|
37
55
|
file: s.file,
|
|
38
56
|
line: s.line,
|
package/dist/core/terminal.d.ts
CHANGED
|
@@ -19,6 +19,29 @@ export declare function inline(text: string): string;
|
|
|
19
19
|
* A space rather than deletion: removing the byte would silently splice
|
|
20
20
|
* `atte` + `st` into a word that was never in the file, and a diagnostic that
|
|
21
21
|
* quietly rewrites what it quotes is its own kind of wrong.
|
|
22
|
+
*
|
|
23
|
+
* The class is written out rather than computed per character. The previous
|
|
24
|
+
* spelling spread the string into a per-code-point array, mapped and rejoined —
|
|
25
|
+
* three allocations proportional to the input, where the engine's own scan needs
|
|
26
|
+
* none when nothing matches, which is the case every real registry is. Measured
|
|
27
|
+
* over 120,000 characters: **27x** faster on plain ASCII, **54x** with
|
|
28
|
+
* newlines, **84x** on CJK. A payload that is *entirely* control bytes is a
|
|
29
|
+
* wash (0.9x), because then there is nothing to fast-path and both spellings
|
|
30
|
+
* build a new string; both are linear either way, so this is a constant factor
|
|
31
|
+
* rather than a second ATX-59.
|
|
32
|
+
*
|
|
33
|
+
* It is worth the change because the count scales with the registry while the
|
|
34
|
+
* length scales with whatever the registry chose: `render`'s `sanitised` calls
|
|
35
|
+
* this once per statement, rationale, param key, param value and out-of-scope
|
|
36
|
+
* entry, on the same reachable `render --check` path ATX-59 came off.
|
|
37
|
+
*
|
|
38
|
+
* **Byte-identical to the spelling it replaces**, which is the only thing that
|
|
39
|
+
* mattered: checked exhaustively over every code unit in the BMP, and over
|
|
40
|
+
* 200,000 randomised strings mixing control bytes, CJK, astral characters and
|
|
41
|
+
* lone surrogates. Surrogates are the case the spread existed to get right —
|
|
42
|
+
* `D800`–`DFFF` fall outside every range below, so neither spelling touches a
|
|
43
|
+
* pair or splits one — and newlines survive here as they always did, by not
|
|
44
|
+
* being in the class.
|
|
22
45
|
*/
|
|
23
46
|
export declare function control(text: string): string;
|
|
24
47
|
//# sourceMappingURL=terminal.d.ts.map
|
package/dist/core/terminal.js
CHANGED
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
// loader has to sanitise Vite's log output for the same reason and cannot
|
|
20
20
|
// import from a layer above it. Two copies of this decision is how the crash
|
|
21
21
|
// path came to be missed once already.
|
|
22
|
-
/** The one control character that survives: see `block`. */
|
|
23
|
-
const NEWLINE = 0x0a;
|
|
24
22
|
/** How far a message is indented under its header line. */
|
|
25
23
|
export const INDENT = ' ';
|
|
26
24
|
/**
|
|
@@ -46,14 +44,32 @@ export function inline(text) {
|
|
|
46
44
|
* A space rather than deletion: removing the byte would silently splice
|
|
47
45
|
* `atte` + `st` into a word that was never in the file, and a diagnostic that
|
|
48
46
|
* quietly rewrites what it quotes is its own kind of wrong.
|
|
47
|
+
*
|
|
48
|
+
* The class is written out rather than computed per character. The previous
|
|
49
|
+
* spelling spread the string into a per-code-point array, mapped and rejoined —
|
|
50
|
+
* three allocations proportional to the input, where the engine's own scan needs
|
|
51
|
+
* none when nothing matches, which is the case every real registry is. Measured
|
|
52
|
+
* over 120,000 characters: **27x** faster on plain ASCII, **54x** with
|
|
53
|
+
* newlines, **84x** on CJK. A payload that is *entirely* control bytes is a
|
|
54
|
+
* wash (0.9x), because then there is nothing to fast-path and both spellings
|
|
55
|
+
* build a new string; both are linear either way, so this is a constant factor
|
|
56
|
+
* rather than a second ATX-59.
|
|
57
|
+
*
|
|
58
|
+
* It is worth the change because the count scales with the registry while the
|
|
59
|
+
* length scales with whatever the registry chose: `render`'s `sanitised` calls
|
|
60
|
+
* this once per statement, rationale, param key, param value and out-of-scope
|
|
61
|
+
* entry, on the same reachable `render --check` path ATX-59 came off.
|
|
62
|
+
*
|
|
63
|
+
* **Byte-identical to the spelling it replaces**, which is the only thing that
|
|
64
|
+
* mattered: checked exhaustively over every code unit in the BMP, and over
|
|
65
|
+
* 200,000 randomised strings mixing control bytes, CJK, astral characters and
|
|
66
|
+
* lone surrogates. Surrogates are the case the spread existed to get right —
|
|
67
|
+
* `D800`–`DFFF` fall outside every range below, so neither spelling touches a
|
|
68
|
+
* pair or splits one — and newlines survive here as they always did, by not
|
|
69
|
+
* being in the class.
|
|
49
70
|
*/
|
|
50
71
|
export function control(text) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const c = ch.codePointAt(0);
|
|
54
|
-
const isControl = (c < 0x20 && c !== NEWLINE) || (c >= 0x7f && c <= 0x9f);
|
|
55
|
-
return isControl ? ' ' : ch;
|
|
56
|
-
})
|
|
57
|
-
.join('');
|
|
72
|
+
// eslint-disable-next-line no-control-regex -- matching control characters is the whole function.
|
|
73
|
+
return text.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f]/g, ' ');
|
|
58
74
|
}
|
|
59
75
|
//# sourceMappingURL=terminal.js.map
|
package/dist/core/validator.d.ts
CHANGED
|
@@ -18,8 +18,12 @@ export declare function uncoveredIssues(registry: Registry, plan: AttestPlan): I
|
|
|
18
18
|
*
|
|
19
19
|
* and one WARNING:
|
|
20
20
|
* - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
|
|
21
|
+
*
|
|
22
|
+
* `registryIncomplete` says that at least one `*.reqs.ts` failed to load, so
|
|
23
|
+
* `registry` is known to be missing whatever was in it. It changes no verdict —
|
|
24
|
+
* only what `orphan-test` advises, for the reason below.
|
|
21
25
|
*/
|
|
22
|
-
export declare function validateStructure(registry: Registry, plan: AttestPlan): Issue[];
|
|
26
|
+
export declare function validateStructure(registry: Registry, plan: AttestPlan, registryIncomplete?: boolean): Issue[];
|
|
23
27
|
/**
|
|
24
28
|
* Weak anti-drift heuristic (design §6, mechanism 3). Evaluated per requirement,
|
|
25
29
|
* not per scenario: a requirement that owns params is quiet as soon as *any* one
|
package/dist/core/validator.js
CHANGED
|
@@ -33,11 +33,32 @@ export function uncoveredIssues(registry, plan) {
|
|
|
33
33
|
*
|
|
34
34
|
* and one WARNING:
|
|
35
35
|
* - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
|
|
36
|
+
*
|
|
37
|
+
* `registryIncomplete` says that at least one `*.reqs.ts` failed to load, so
|
|
38
|
+
* `registry` is known to be missing whatever was in it. It changes no verdict —
|
|
39
|
+
* only what `orphan-test` advises, for the reason below.
|
|
36
40
|
*/
|
|
37
|
-
export function validateStructure(registry, plan) {
|
|
41
|
+
export function validateStructure(registry, plan, registryIncomplete = false) {
|
|
38
42
|
const issues = [];
|
|
39
43
|
const knownIds = new Set(Object.keys(registry));
|
|
40
44
|
// orphan-test: covers a requirement that does not exist.
|
|
45
|
+
//
|
|
46
|
+
// "Add it to the registry, or fix the id" is the right advice for an id that
|
|
47
|
+
// is genuinely absent, and **wrong** for one already sitting in a registry
|
|
48
|
+
// file that failed to load: the id is there, the fix is the load error, and
|
|
49
|
+
// following the hint would add a duplicate. That case is not rare when it
|
|
50
|
+
// happens — one unreadable `*.reqs.ts` orphans every scenario of every
|
|
51
|
+
// requirement it declared, so the wrong advice is also the loudest thing in
|
|
52
|
+
// the report.
|
|
53
|
+
//
|
|
54
|
+
// Which of the two this is cannot be decided here: the ids of a file that
|
|
55
|
+
// never parsed are exactly what is unavailable. So the finding stands and the
|
|
56
|
+
// *advice* names the uncertainty and the order to work in — a report that is
|
|
57
|
+
// quieter about problems it can still see would be the worse trade for a
|
|
58
|
+
// command whose contract is breadth.
|
|
59
|
+
const orphanFix = (id) => registryIncomplete
|
|
60
|
+
? `A registry file failed to load, so ids it declares are missing here — fix that first, and add "${id}" only if it is still unknown afterwards.`
|
|
61
|
+
: `Add it to the registry, or fix the id.`;
|
|
41
62
|
for (const s of plan.scenarios) {
|
|
42
63
|
if (!knownIds.has(s.reqId)) {
|
|
43
64
|
issues.push({
|
|
@@ -46,7 +67,7 @@ export function validateStructure(registry, plan) {
|
|
|
46
67
|
reqId: s.reqId,
|
|
47
68
|
file: s.file,
|
|
48
69
|
line: s.line,
|
|
49
|
-
message: `scenario "${s.name}" attests unknown requirement "${s.reqId}".
|
|
70
|
+
message: `scenario "${s.name}" attests unknown requirement "${s.reqId}". ${orphanFix(s.reqId)}`,
|
|
50
71
|
});
|
|
51
72
|
}
|
|
52
73
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Group scenarios that verify one requirement. The describe block is named
|
|
3
3
|
* `[id]` so runtime coverage can be reconstructed from the task tree (§5.4).
|
|
4
|
+
*
|
|
5
|
+
* The body is handed to `describe` untouched. It used to be wrapped in a
|
|
6
|
+
* set-and-restore of a module-level `currentReqId` that `scenario()` read; that
|
|
7
|
+
* variable is deleted rather than repaired — see `scenario`.
|
|
4
8
|
*/
|
|
5
9
|
export declare function requirement(id: string, body: () => void): void;
|
|
6
10
|
/**
|
|
7
11
|
* Declare a single verifiable scenario. Must be nested inside requirement().
|
|
8
12
|
* The assertion itself is delegated to Vitest `it` (design §3).
|
|
9
13
|
*
|
|
14
|
+
* **The nesting check happens when the scenario runs, not when it is
|
|
15
|
+
* collected**, and that is what makes grouping work. Vitest defers a nested
|
|
16
|
+
* `describe`'s callback until after the parent's has returned — measured: at
|
|
17
|
+
* the end of the outer callback the inner one has not run — so a module-level
|
|
18
|
+
* `currentReqId` set and restored around `body()` was already back to `null` by
|
|
19
|
+
* the time a grouped `scenario()` ran. It threw, and a throw during collection
|
|
20
|
+
* is a *file* error: nothing in that file was collected, so every scenario in
|
|
21
|
+
* it came back `declared-not-run` — including correct ungrouped ones under
|
|
22
|
+
* other requirements — each blamed on "skipped, or excluded by an .only?".
|
|
23
|
+
* Meanwhile `check` and `cover` read the same file statically and reported full
|
|
24
|
+
* coverage, so the static half called a file green that could not execute at
|
|
25
|
+
* all.
|
|
26
|
+
*
|
|
27
|
+
* Checking here costs nothing and fails better: a scenario genuinely outside
|
|
28
|
+
* any `requirement()` now fails as one test instead of taking its file down.
|
|
29
|
+
*
|
|
10
30
|
* `timeoutMs` is passed straight through to `it`. It exists because `verify`
|
|
11
31
|
* runs the suite **isolated** (§5.4) — no `vitest.config.ts` unless
|
|
12
32
|
* `--vitest-config` names one — so Vitest's 5-second default is what every
|
package/dist/runtime.js
CHANGED
|
@@ -2,27 +2,53 @@
|
|
|
2
2
|
// Pure delegation to Vitest; no in-process coverage singleton — coverage is
|
|
3
3
|
// reconstructed from the Vitest task tree instead (design §5.4).
|
|
4
4
|
import { describe, it } from 'vitest';
|
|
5
|
-
|
|
5
|
+
import { requirementIdOf, requirementSuiteName } from './core/req-suite.js';
|
|
6
6
|
/**
|
|
7
7
|
* Group scenarios that verify one requirement. The describe block is named
|
|
8
8
|
* `[id]` so runtime coverage can be reconstructed from the task tree (§5.4).
|
|
9
|
+
*
|
|
10
|
+
* The body is handed to `describe` untouched. It used to be wrapped in a
|
|
11
|
+
* set-and-restore of a module-level `currentReqId` that `scenario()` read; that
|
|
12
|
+
* variable is deleted rather than repaired — see `scenario`.
|
|
9
13
|
*/
|
|
10
14
|
export function requirement(id, body) {
|
|
11
|
-
describe(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
describe(requirementSuiteName(id), body);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The requirement a scenario actually ended up inside, read from the task tree.
|
|
19
|
+
*
|
|
20
|
+
* The same source `runner.ts` attributes coverage from, which is the point: a
|
|
21
|
+
* module-level "current requirement" was a *second* answer to a question the
|
|
22
|
+
* suite name already answers, and the two disagreed exactly where it mattered.
|
|
23
|
+
*/
|
|
24
|
+
function owningRequirement(task) {
|
|
25
|
+
for (let s = task?.suite; s; s = s.suite) {
|
|
26
|
+
const id = requirementIdOf(s.name ?? '');
|
|
27
|
+
if (id !== undefined)
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
33
|
* Declare a single verifiable scenario. Must be nested inside requirement().
|
|
24
34
|
* The assertion itself is delegated to Vitest `it` (design §3).
|
|
25
35
|
*
|
|
36
|
+
* **The nesting check happens when the scenario runs, not when it is
|
|
37
|
+
* collected**, and that is what makes grouping work. Vitest defers a nested
|
|
38
|
+
* `describe`'s callback until after the parent's has returned — measured: at
|
|
39
|
+
* the end of the outer callback the inner one has not run — so a module-level
|
|
40
|
+
* `currentReqId` set and restored around `body()` was already back to `null` by
|
|
41
|
+
* the time a grouped `scenario()` ran. It threw, and a throw during collection
|
|
42
|
+
* is a *file* error: nothing in that file was collected, so every scenario in
|
|
43
|
+
* it came back `declared-not-run` — including correct ungrouped ones under
|
|
44
|
+
* other requirements — each blamed on "skipped, or excluded by an .only?".
|
|
45
|
+
* Meanwhile `check` and `cover` read the same file statically and reported full
|
|
46
|
+
* coverage, so the static half called a file green that could not execute at
|
|
47
|
+
* all.
|
|
48
|
+
*
|
|
49
|
+
* Checking here costs nothing and fails better: a scenario genuinely outside
|
|
50
|
+
* any `requirement()` now fails as one test instead of taking its file down.
|
|
51
|
+
*
|
|
26
52
|
* `timeoutMs` is passed straight through to `it`. It exists because `verify`
|
|
27
53
|
* runs the suite **isolated** (§5.4) — no `vitest.config.ts` unless
|
|
28
54
|
* `--vitest-config` names one — so Vitest's 5-second default is what every
|
|
@@ -32,9 +58,11 @@ export function requirement(id, body) {
|
|
|
32
58
|
* for the whole suite, sets one number for what is a property of one scenario.
|
|
33
59
|
*/
|
|
34
60
|
export function scenario(name, fn, timeoutMs) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
it(name, async (ctx) => {
|
|
62
|
+
if (owningRequirement(ctx.task) === undefined) {
|
|
63
|
+
throw new Error(`scenario("${name}") must be nested inside requirement(...)`);
|
|
64
|
+
}
|
|
65
|
+
return fn();
|
|
66
|
+
}, timeoutMs);
|
|
39
67
|
}
|
|
40
68
|
//# sourceMappingURL=runtime.js.map
|
package/package.json
CHANGED