@jterrazz/test 13.0.2 → 14.0.1
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 +20 -20
- package/dist/checker.js +1079 -895
- package/dist/index.d.ts +18 -17
- package/dist/index.js +23 -23
- package/dist/literate.js +26 -21
- package/dist/match.js +44 -7
- package/dist/oxlint.cjs +193 -43
- package/dist/oxlint.js +193 -43
- package/dist/spec-document.js +85 -6
- package/package.json +1 -1
- package/schema/spec.schema.json +1 -1
package/dist/oxlint.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
* Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
|
|
7
7
|
*
|
|
8
8
|
* Rules only ever look at directories NEXT TO files oxlint is already
|
|
9
|
-
* visiting (a feature's `
|
|
9
|
+
* visiting (a feature's `_requests/`, `_seeds/`, `_expected/`, a sibling module
|
|
10
10
|
* file), so the probe set is small — but several rules probe the same feature
|
|
11
11
|
* directory, and oxlint lints many files per process. A module-level cache
|
|
12
12
|
* keeps each `readdir`/`stat` to one syscall per lint run.
|
|
@@ -337,7 +337,7 @@ const RULE_DOCS = {
|
|
|
337
337
|
},
|
|
338
338
|
"c1-domain-structure": {
|
|
339
339
|
channel: "statique",
|
|
340
|
-
convention: "La forme de l’arbre `specs/` se déclare via l’option `depth` : `facet-domain` (défaut — `*.test.ts` à la profondeur facet/domain, `*.specification.ts` au root de la facette), `facet` (test au root de la facette OU un dossier domaine plus bas, jamais plus profond), `mirror` (test à toute profondeur ≥ 1, nommé d’après son dossier), `off`.",
|
|
340
|
+
convention: "La forme de l’arbre `specs/` se déclare via l’option `depth` : `facet-domain` (défaut — `*.test.ts` à la profondeur facet/domain, `*.specification.ts` au root de la facette), `facet` (test au root de la facette OU un dossier domaine plus bas, jamais plus profond), `mirror` (test à toute profondeur ≥ 1, nommé d’après son dossier), `off`. Dans tous les modes, `off` compris : un dossier à underscore initial est du SOL, jamais un domaine — aucune spec ne vit dedans. Une exception, et une seule : le sol peut être du CODE, et un `<module>.test.ts` posé À CÔTÉ du `<module>.ts` qu’il teste y est légal (c’est la loi I2) ; un test sans module voisin, ou un `*.specification.ts`, reste une violation.",
|
|
341
341
|
family: "C",
|
|
342
342
|
id: "C1",
|
|
343
343
|
rationale: "Une profondeur fixe rend la place de chaque fichier prévisible et détectable statiquement."
|
|
@@ -356,9 +356,16 @@ const RULE_DOCS = {
|
|
|
356
356
|
id: "C11",
|
|
357
357
|
rationale: "Une donnée n’existe que servie par un contrat — un fichier sans propriétaire est du poids mort qu’aucun test ne charge."
|
|
358
358
|
},
|
|
359
|
+
"c13-underscored-ground": {
|
|
360
|
+
channel: "statique",
|
|
361
|
+
convention: "Sous `specs/`, le sol d’une spec porte un underscore initial : `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`. Un dossier `fixtures/`, `expected/`, `requests/` ou `seeds/` est un nom d’avant la 14 — erreur nommant le renommage.",
|
|
362
|
+
family: "C",
|
|
363
|
+
id: "C13",
|
|
364
|
+
rationale: "Aucun résolveur ne lit plus le nom sans underscore : gardé tel quel, l’arbre devient invisible plutôt que faux, et l’échec arrive une étape plus loin que sa cause."
|
|
365
|
+
},
|
|
359
366
|
"c2-http-only-requests": {
|
|
360
367
|
channel: "statique",
|
|
361
|
-
convention: "`
|
|
368
|
+
convention: "`_requests/` ne contient que des fichiers `.http` ; toute autre extension est une erreur.",
|
|
362
369
|
family: "C",
|
|
363
370
|
id: "C2",
|
|
364
371
|
rationale: "Une entrée de requête est un `.http` complet — homogénéiser le dossier interdit les formats ad hoc."
|
|
@@ -375,11 +382,11 @@ const RULE_DOCS = {
|
|
|
375
382
|
convention: "L’argument de `toMatch` porte son extension (`'help.txt'`), sauf pour les snapshots d’arborescence (dossiers) ; un sujet fichier sans extension est une erreur.",
|
|
376
383
|
family: "C",
|
|
377
384
|
id: "C6",
|
|
378
|
-
rationale: "L’extension fait partie du nom du fichier attendu — l’omettre casse la résolution `
|
|
385
|
+
rationale: "L’extension fait partie du nom du fichier attendu — l’omettre casse la résolution `_expected/`."
|
|
379
386
|
},
|
|
380
387
|
"c7-seeds-sql-only": {
|
|
381
388
|
channel: "statique",
|
|
382
|
-
convention: "`
|
|
389
|
+
convention: "`_seeds/` ne contient que des `*.sql` ; tout autre fichier est une erreur.",
|
|
383
390
|
family: "C",
|
|
384
391
|
id: "C7",
|
|
385
392
|
rationale: "`.seed()` porte l’état des bases uniquement — pas de seed-handler ni de dispatch par préfixe."
|
|
@@ -421,7 +428,7 @@ const RULE_DOCS = {
|
|
|
421
428
|
},
|
|
422
429
|
"d9w-single-use-ref": {
|
|
423
430
|
channel: "statique",
|
|
424
|
-
convention: "Une ref de capture (`match.ref`, `{{kind#ref}}`) qui n’apparaît qu’une seule fois dans tout le fichier (code + fixtures `
|
|
431
|
+
convention: "Une ref de capture (`match.ref`, `{{kind#ref}}`) qui n’apparaît qu’une seule fois dans tout le fichier (code + fixtures `_expected/` référencées) porte un nom inutilement → warning.",
|
|
425
432
|
family: "D",
|
|
426
433
|
id: "D9",
|
|
427
434
|
rationale: "Une ref ne se justifie que si elle asserte l’égalité sur au moins deux occurrences."
|
|
@@ -563,7 +570,7 @@ const RULE_DOCS = {
|
|
|
563
570
|
const CHECKER_PASSES = [
|
|
564
571
|
{
|
|
565
572
|
channel: "checker",
|
|
566
|
-
convention: "Tout `{{token}}` dans une fixture `
|
|
573
|
+
convention: "Tout `{{token}}` dans une fixture `_expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
|
|
567
574
|
family: "D",
|
|
568
575
|
id: "D4",
|
|
569
576
|
name: "d4-unknown-token",
|
|
@@ -571,7 +578,7 @@ const CHECKER_PASSES = [
|
|
|
571
578
|
},
|
|
572
579
|
{
|
|
573
580
|
channel: "checker",
|
|
574
|
-
convention: "Une ref malformée d’un kind connu (`{{iso8601#}}`, `{{uuid #id}}`) dans un fichier texte sous `
|
|
581
|
+
convention: "Une ref malformée d’un kind connu (`{{iso8601#}}`, `{{uuid #id}}`) dans un fichier texte sous `_expected/` est une erreur.",
|
|
575
582
|
family: "D",
|
|
576
583
|
id: "D4",
|
|
577
584
|
name: "d4-malformed-ref",
|
|
@@ -579,7 +586,7 @@ const CHECKER_PASSES = [
|
|
|
579
586
|
},
|
|
580
587
|
{
|
|
581
588
|
channel: "checker",
|
|
582
|
-
convention: "La première ligne d’un `.http` de profondeur 1 suit sa grammaire : requête (`MÉTHODE /path`) sous `
|
|
589
|
+
convention: "La première ligne d’un `.http` de profondeur 1 suit sa grammaire : requête (`MÉTHODE /path`) sous `_requests/`, statut (`HTTP/1.1 <status>`) sous `_expected/`.",
|
|
583
590
|
family: "D",
|
|
584
591
|
id: "D4b",
|
|
585
592
|
name: "d4b-http-first-line",
|
|
@@ -675,7 +682,7 @@ const CHECKER_PASSES = [
|
|
|
675
682
|
},
|
|
676
683
|
{
|
|
677
684
|
channel: "checker",
|
|
678
|
-
convention: "Un token connu dans un fichier sous `
|
|
685
|
+
convention: "Un token connu dans un fichier sous `_requests/` → warning : les requêtes sont des entrées, jamais matchées.",
|
|
679
686
|
family: "D",
|
|
680
687
|
id: "D10",
|
|
681
688
|
name: "d10w-tokens-in-requests",
|
|
@@ -683,12 +690,28 @@ const CHECKER_PASSES = [
|
|
|
683
690
|
},
|
|
684
691
|
{
|
|
685
692
|
channel: "checker",
|
|
686
|
-
convention: "Aucune fixture morte : tout fichier sous `
|
|
693
|
+
convention: "Aucune fixture morte : tout fichier sous `_seeds/`/`_requests/`/`_fixtures/` et toute entrée de premier niveau de `_expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
|
|
687
694
|
family: "C",
|
|
688
695
|
id: "C9",
|
|
689
696
|
name: "c9-dead-fixtures",
|
|
690
697
|
rationale: "Le miroir de C8 — une fixture que rien ne référence est du poids mort qui trompe le lecteur."
|
|
691
698
|
},
|
|
699
|
+
{
|
|
700
|
+
channel: "checker",
|
|
701
|
+
convention: "Une fixture du pool est PARTAGÉE, ou elle est locale : un dossier de `<specs>/_fixtures/` référencé (`$FIXTURES/<nom>`) depuis un seul dossier de spec — deux documents d’une même feuille comptent pour un — doit vivre à côté de cette feuille, en `<feuille>/_fixtures/<nom>/`, référencé par la forme relative. Zéro référence reste l’erreur de fixture morte (C9). Corrigé par `--fix` : le dossier est déplacé et les littéraux réécrits.",
|
|
702
|
+
family: "C",
|
|
703
|
+
id: "C14",
|
|
704
|
+
name: "c14-pool-fixture-shared",
|
|
705
|
+
rationale: "Une fixture d’un seul scénario garée dans le pool se lit comme du sol partagé : chacun suppose qu’un autre en dépend et personne n’ose y toucher."
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
channel: "checker",
|
|
709
|
+
convention: "Une fixture locale ne se lit que depuis son propre dossier : un chemin `fixture:`/`.fixture()` contenant `..`, ou dont la cible sort du `_fixtures/` de la spec qui le nomme, est une erreur — le sol partagé par plusieurs feuilles va dans le pool `$FIXTURES`.",
|
|
710
|
+
family: "C",
|
|
711
|
+
id: "C15",
|
|
712
|
+
name: "c15-local-fixture-reach",
|
|
713
|
+
rationale: "Une feuille qui pioche dans les fixtures d’une voisine, c’est le pool par une autre porte, sans la visibilité du pool : la voisine casse une spec à deux dossiers de là sans le savoir."
|
|
714
|
+
},
|
|
692
715
|
{
|
|
693
716
|
channel: "checker",
|
|
694
717
|
convention: "Canal principal de B5 : les runners docker-aware sont inférés de l’option `docker:` du fichier de specification importé, puis chaque résultat `.exec()` lié sans `await using` est signalé.",
|
|
@@ -1232,16 +1255,58 @@ const a9wRedundantRoot = {
|
|
|
1232
1255
|
}
|
|
1233
1256
|
};
|
|
1234
1257
|
//#endregion
|
|
1258
|
+
//#region src/core/specification/shared/ground.ts
|
|
1259
|
+
/**
|
|
1260
|
+
* The GROUND a row of specs stands on — the naming law of a spec tree.
|
|
1261
|
+
*
|
|
1262
|
+
* In a row of specs, a MEMBER is a spec: `<aspect>.test.ts`, `<case>.spec.yaml`,
|
|
1263
|
+
* `<name>.specification.ts`, and the facet/domain folders that hold them.
|
|
1264
|
+
* Everything else in that row is what the members STAND ON — inert material the
|
|
1265
|
+
* framework resolves BY PATH, never imported, never executed — and it carries a
|
|
1266
|
+
* LEADING UNDERSCORE. A spec's own folder never does.
|
|
1267
|
+
*
|
|
1268
|
+
* Four names, and only four: `_fixtures/` (file state), `_expected/` (goldens),
|
|
1269
|
+
* `_requests/` (inputs), `_seeds/` (database state). `contracts/` is NOT ground —
|
|
1270
|
+
* a contract is TypeScript a spec imports, behaviour composed with the spec's own,
|
|
1271
|
+
* so it stays a member. `docker/` is not ground either: it sits at the PROJECT
|
|
1272
|
+
* root beside `package.json`, outside any row of specs.
|
|
1273
|
+
*
|
|
1274
|
+
* This module is the single home of those names. Every resolver, matcher, lint
|
|
1275
|
+
* rule and checker pass reads them from here, so the runtime and the static
|
|
1276
|
+
* channels cannot disagree about where a spec's ground lives.
|
|
1277
|
+
*/
|
|
1278
|
+
/** Goldens: every expected fixture, flat, extension included. */
|
|
1279
|
+
const GROUND_EXPECTED = "_expected";
|
|
1280
|
+
/** File state copied into a working directory by `.fixture()`. */
|
|
1281
|
+
const GROUND_FIXTURES = "_fixtures";
|
|
1282
|
+
/** Inputs: complete `.http` request documents. */
|
|
1283
|
+
const GROUND_REQUESTS = "_requests";
|
|
1284
|
+
/** Database state: `.sql` fragments loaded by `.seed()`. */
|
|
1285
|
+
const GROUND_SEEDS = "_seeds";
|
|
1286
|
+
/**
|
|
1287
|
+
* The pre-14 name of each ground directory → the name it carries now. Kept for
|
|
1288
|
+
* ONE purpose: naming the rename in a diagnostic (`c13-underscored-ground`).
|
|
1289
|
+
* Nothing resolves through it — an un-underscored directory is invisible to
|
|
1290
|
+
* every resolver, which is exactly why it must be reported rather than tolerated.
|
|
1291
|
+
*/
|
|
1292
|
+
const RENAMED_GROUND_DIRS = {
|
|
1293
|
+
expected: GROUND_EXPECTED,
|
|
1294
|
+
fixtures: GROUND_FIXTURES,
|
|
1295
|
+
requests: GROUND_REQUESTS,
|
|
1296
|
+
seeds: GROUND_SEEDS
|
|
1297
|
+
};
|
|
1298
|
+
//#endregion
|
|
1235
1299
|
//#region src/core/specification/shared/fixtures.ts
|
|
1236
1300
|
/**
|
|
1237
1301
|
* Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
|
|
1238
1302
|
*
|
|
1239
1303
|
* A fixture path is one of two shapes:
|
|
1240
1304
|
*
|
|
1241
|
-
* - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/
|
|
1305
|
+
* - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/_fixtures/<rest>`,
|
|
1242
1306
|
* where `<specs-root>` is the nearest ancestor directory named `specs`.
|
|
1243
|
-
* - `<path>` (no marker) — feature-local, at `<test-dir>/
|
|
1307
|
+
* - `<path>` (no marker) — feature-local, at `<test-dir>/_fixtures/<path>`.
|
|
1244
1308
|
*
|
|
1309
|
+
* The leading underscore is the ground rule of a spec tree ({@link GROUND_FIXTURES}).
|
|
1245
1310
|
* Any other `$`-prefixed marker is a usage error (listed against the known
|
|
1246
1311
|
* markers). Copy semantics mirror rsync's trailing-slash rule (see
|
|
1247
1312
|
* {@link copyPlan}).
|
|
@@ -1535,6 +1600,14 @@ const TEST_SUFFIX = ".test.ts";
|
|
|
1535
1600
|
const SPECIFICATION_SUFFIX = ".specification.ts";
|
|
1536
1601
|
const DEFAULT_DEPTH = "facet-domain";
|
|
1537
1602
|
/**
|
|
1603
|
+
* Whether a file is the unit test of the module it sits beside — `x.test.ts`
|
|
1604
|
+
* next to `x.ts`, which is the whole of I2's pairing. This is what tells a
|
|
1605
|
+
* ground MODULE's test from a spec that wandered into the ground.
|
|
1606
|
+
*/
|
|
1607
|
+
function testsItsNeighbour(filename) {
|
|
1608
|
+
return filename.endsWith(TEST_SUFFIX) && isFile(`${filename.slice(0, -8)}.ts`);
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1538
1611
|
* CONVENTIONS C1' — the shape of a spec tree, DECLARED by the consumer.
|
|
1539
1612
|
*
|
|
1540
1613
|
* A project states which shape its `specs/` tree has; the rule checks that one.
|
|
@@ -1576,7 +1649,18 @@ const DEFAULT_DEPTH = "facet-domain";
|
|
|
1576
1649
|
* specs root. Specification files are unconstrained: a mirror has no facet
|
|
1577
1650
|
* level to anchor them to.
|
|
1578
1651
|
*
|
|
1579
|
-
* `depth: 'off'` — no
|
|
1652
|
+
* `depth: 'off'` — no DEPTH check; the tree's shape is guarded elsewhere.
|
|
1653
|
+
*
|
|
1654
|
+
* The ground clause holds in every mode, `off` included: a directory whose name
|
|
1655
|
+
* carries a LEADING UNDERSCORE is ground — what the specs of a row stand on —
|
|
1656
|
+
* and never a domain, so no SPEC may live inside one. That is not a question of
|
|
1657
|
+
* depth, which is why no project's declared shape switches it off.
|
|
1658
|
+
*
|
|
1659
|
+
* Ground is not always inert, though: it may be CODE the specs stand on — the
|
|
1660
|
+
* build of the subject under test, a harness the runner spawns. A `<module>.ts`
|
|
1661
|
+
* there keeps its own unit test under I2's law, `<module>.test.ts` NEXT to it,
|
|
1662
|
+
* and the clause lets that one pair through. Nothing else: a test with no module
|
|
1663
|
+
* beside it, or a `*.specification.ts`, is a spec that wandered into the ground.
|
|
1580
1664
|
*
|
|
1581
1665
|
* Module tests under `src/` follow the neighbour rule (I2) and are out of scope
|
|
1582
1666
|
* in every mode.
|
|
@@ -1584,12 +1668,23 @@ const DEFAULT_DEPTH = "facet-domain";
|
|
|
1584
1668
|
const c1DomainStructure = {
|
|
1585
1669
|
create(context) {
|
|
1586
1670
|
const depth = context.options[0]?.depth ?? DEFAULT_DEPTH;
|
|
1587
|
-
if (depth === "off") return {};
|
|
1588
1671
|
return { Program(node) {
|
|
1589
1672
|
const anchor = specsAnchor(context.filename);
|
|
1590
1673
|
if (anchor === void 0) return;
|
|
1591
1674
|
const base = anchor.relative.at(-1) ?? "";
|
|
1592
1675
|
const nesting = anchor.relative.length - 1;
|
|
1676
|
+
if (base.endsWith(TEST_SUFFIX) || base.endsWith(SPECIFICATION_SUFFIX)) {
|
|
1677
|
+
const ground = anchor.relative.slice(0, -1).find((segment) => segment.startsWith("_"));
|
|
1678
|
+
if (ground !== void 0) {
|
|
1679
|
+
if (!testsItsNeighbour(context.filename)) context.report({
|
|
1680
|
+
data: { ground },
|
|
1681
|
+
messageId: "specInGround",
|
|
1682
|
+
node
|
|
1683
|
+
});
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (depth === "off") return;
|
|
1593
1688
|
if (depth === "facet") {
|
|
1594
1689
|
if (base.endsWith(TEST_SUFFIX)) {
|
|
1595
1690
|
if (nesting < 1) context.report({
|
|
@@ -1648,6 +1743,7 @@ const c1DomainStructure = {
|
|
|
1648
1743
|
defaultOptions: [{ depth: DEFAULT_DEPTH }],
|
|
1649
1744
|
docs: RULE_DOCS["c1-domain-structure"],
|
|
1650
1745
|
messages: {
|
|
1746
|
+
specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain (C1 — see docs/10-linting.md). Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
|
|
1651
1747
|
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
|
|
1652
1748
|
testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (C1 — see docs/10-linting.md).",
|
|
1653
1749
|
testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing (C1 — see docs/10-linting.md).",
|
|
@@ -1705,7 +1801,7 @@ const c10ContractsBoundary = {
|
|
|
1705
1801
|
};
|
|
1706
1802
|
//#endregion
|
|
1707
1803
|
//#region src/lint/rules/c11-contract-data-pairing.ts
|
|
1708
|
-
const TEST_FILE$
|
|
1804
|
+
const TEST_FILE$12 = /\.test\.[cm]?[jt]sx?$/;
|
|
1709
1805
|
/** The provider folders that may carry data files. */
|
|
1710
1806
|
const PROVIDERS$1 = [
|
|
1711
1807
|
"anthropic",
|
|
@@ -1729,7 +1825,7 @@ function ownerOf(entry) {
|
|
|
1729
1825
|
const c11ContractDataPairing = {
|
|
1730
1826
|
create(context) {
|
|
1731
1827
|
const file = context.physicalFilename;
|
|
1732
|
-
if (!TEST_FILE$
|
|
1828
|
+
if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
|
|
1733
1829
|
return { Program(node) {
|
|
1734
1830
|
const contractsDir = join(dirname(file), "contracts");
|
|
1735
1831
|
for (const provider of PROVIDERS$1) {
|
|
@@ -1759,13 +1855,66 @@ const c11ContractDataPairing = {
|
|
|
1759
1855
|
}
|
|
1760
1856
|
};
|
|
1761
1857
|
//#endregion
|
|
1858
|
+
//#region src/lint/rules/c13-underscored-ground.ts
|
|
1859
|
+
const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/;
|
|
1860
|
+
/**
|
|
1861
|
+
* CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
|
|
1862
|
+
* directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
|
|
1863
|
+
* `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
|
|
1864
|
+
* `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
|
|
1865
|
+
* fallback.
|
|
1866
|
+
*
|
|
1867
|
+
* The catch this rule exists for: a consumer that upgrades and keeps the old
|
|
1868
|
+
* names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
|
|
1869
|
+
* `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
|
|
1870
|
+
* and the tree becomes invisible rather than wrong. The failure that follows is
|
|
1871
|
+
* "fixture does not exist" on every spec at once, one step removed from the
|
|
1872
|
+
* cause; this rule states the cause and the rename.
|
|
1873
|
+
*
|
|
1874
|
+
* Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
|
|
1875
|
+
* spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
|
|
1876
|
+
* nearest specs root. A DOMAIN folder named after the ground it holds
|
|
1877
|
+
* (`specs/api/requests/_requests/`) is never a probe target — only what sits
|
|
1878
|
+
* beside a spec is.
|
|
1879
|
+
*/
|
|
1880
|
+
const c13UnderscoredGround = {
|
|
1881
|
+
create(context) {
|
|
1882
|
+
const file = context.physicalFilename;
|
|
1883
|
+
const anchor = specsAnchor(file);
|
|
1884
|
+
if (!TEST_FILE$11.test(file) || anchor === void 0) return {};
|
|
1885
|
+
return { Program(node) {
|
|
1886
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1887
|
+
const probe = (parent, legacy) => {
|
|
1888
|
+
const path = join(parent, legacy);
|
|
1889
|
+
if (seen.has(path) || !isDirectory(path)) return;
|
|
1890
|
+
seen.add(path);
|
|
1891
|
+
context.report({
|
|
1892
|
+
data: {
|
|
1893
|
+
ground: RENAMED_GROUND_DIRS[legacy],
|
|
1894
|
+
legacy
|
|
1895
|
+
},
|
|
1896
|
+
messageId: "notUnderscored",
|
|
1897
|
+
node
|
|
1898
|
+
});
|
|
1899
|
+
};
|
|
1900
|
+
for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe(dirname(file), legacy);
|
|
1901
|
+
probe(anchor.directory, "fixtures");
|
|
1902
|
+
} };
|
|
1903
|
+
},
|
|
1904
|
+
meta: {
|
|
1905
|
+
docs: RULE_DOCS["c13-underscored-ground"],
|
|
1906
|
+
messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/10-linting.md)." },
|
|
1907
|
+
type: "problem"
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
//#endregion
|
|
1762
1911
|
//#region src/lint/rules/c2-http-only-requests.ts
|
|
1763
1912
|
const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/;
|
|
1764
1913
|
/**
|
|
1765
|
-
* CONVENTIONS C2 — `
|
|
1914
|
+
* CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
|
|
1766
1915
|
* a COMPLETE request: method, path, headers, body). Anchored on the feature's
|
|
1767
1916
|
* visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
|
|
1768
|
-
* `
|
|
1917
|
+
* `_requests/` directory is probed (cached readdir) and any non-`.http` entry
|
|
1769
1918
|
* is reported on the test file.
|
|
1770
1919
|
*/
|
|
1771
1920
|
const c2HttpOnlyRequests = {
|
|
@@ -1773,7 +1922,7 @@ const c2HttpOnlyRequests = {
|
|
|
1773
1922
|
const file = context.physicalFilename;
|
|
1774
1923
|
if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
|
|
1775
1924
|
return { Program(node) {
|
|
1776
|
-
const requestsDir = join(dirname(file),
|
|
1925
|
+
const requestsDir = join(dirname(file), GROUND_REQUESTS);
|
|
1777
1926
|
for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
|
|
1778
1927
|
data: { entry },
|
|
1779
1928
|
messageId: "notHttp",
|
|
@@ -1783,7 +1932,7 @@ const c2HttpOnlyRequests = {
|
|
|
1783
1932
|
},
|
|
1784
1933
|
meta: {
|
|
1785
1934
|
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1786
|
-
messages: { notHttp: "
|
|
1935
|
+
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
|
|
1787
1936
|
type: "problem"
|
|
1788
1937
|
}
|
|
1789
1938
|
};
|
|
@@ -2041,11 +2190,11 @@ function subjectIsDirectory(toMatchCallee) {
|
|
|
2041
2190
|
/**
|
|
2042
2191
|
* CONVENTIONS C6 — the extension is part of the fixture name and mandatory in
|
|
2043
2192
|
* `toMatch` (`'help.txt'`, never `'help'`) — except for directory-tree
|
|
2044
|
-
* snapshots, which are directories under `
|
|
2193
|
+
* snapshots, which are directories under `_expected/`.
|
|
2045
2194
|
*
|
|
2046
2195
|
* The directory exception is resolved two ways: statically, when the expect
|
|
2047
2196
|
* subject visibly chains `.directory(…)`; else on disk, when
|
|
2048
|
-
* `
|
|
2197
|
+
* `_expected/<name>/` exists next to the spec (cached stat).
|
|
2049
2198
|
*/
|
|
2050
2199
|
const c6ToMatchExtension = {
|
|
2051
2200
|
create(context) {
|
|
@@ -2059,7 +2208,7 @@ const c6ToMatchExtension = {
|
|
|
2059
2208
|
if (name === void 0 || name.length === 0) return;
|
|
2060
2209
|
if ((name.split("/").at(-1) ?? "").includes(".")) return;
|
|
2061
2210
|
if (subjectIsDirectory(callee)) return;
|
|
2062
|
-
if (isDirectory(join(dirname(file), "
|
|
2211
|
+
if (isDirectory(join(dirname(file), "_expected", name))) return;
|
|
2063
2212
|
context.report({
|
|
2064
2213
|
data: { name },
|
|
2065
2214
|
messageId: "missingExtension",
|
|
@@ -2069,7 +2218,7 @@ const c6ToMatchExtension = {
|
|
|
2069
2218
|
},
|
|
2070
2219
|
meta: {
|
|
2071
2220
|
docs: RULE_DOCS["c6-tomatch-extension"],
|
|
2072
|
-
messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (
|
|
2221
|
+
messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (_expected/<name>/) omit it (C6 — see docs/10-linting.md)." },
|
|
2073
2222
|
type: "problem"
|
|
2074
2223
|
}
|
|
2075
2224
|
};
|
|
@@ -2077,17 +2226,17 @@ const c6ToMatchExtension = {
|
|
|
2077
2226
|
//#region src/lint/rules/c7-seeds-sql-only.ts
|
|
2078
2227
|
const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/;
|
|
2079
2228
|
/**
|
|
2080
|
-
* CONVENTIONS C7 — `.seed()` carries DATABASE state only: `
|
|
2229
|
+
* CONVENTIONS C7 — `.seed()` carries DATABASE state only: `_seeds/` contains
|
|
2081
2230
|
* nothing but `*.sql` fragments. File state goes through `.fixture()`; there
|
|
2082
2231
|
* is no seed handler and no prefix dispatch. Anchored on the feature's visited
|
|
2083
|
-
* test file (cached readdir of the sibling `
|
|
2232
|
+
* test file (cached readdir of the sibling `_seeds/`).
|
|
2084
2233
|
*/
|
|
2085
2234
|
const c7SeedsSqlOnly = {
|
|
2086
2235
|
create(context) {
|
|
2087
2236
|
const file = context.physicalFilename;
|
|
2088
2237
|
if (!TEST_FILE$7.test(file) || !segments(file).includes("specs")) return {};
|
|
2089
2238
|
return { Program(node) {
|
|
2090
|
-
const seedsDir = join(dirname(file),
|
|
2239
|
+
const seedsDir = join(dirname(file), GROUND_SEEDS);
|
|
2091
2240
|
for (const entry of listDirectory(seedsDir) ?? []) if (!entry.endsWith(".sql")) context.report({
|
|
2092
2241
|
data: { entry },
|
|
2093
2242
|
messageId: "notSql",
|
|
@@ -2097,18 +2246,18 @@ const c7SeedsSqlOnly = {
|
|
|
2097
2246
|
},
|
|
2098
2247
|
meta: {
|
|
2099
2248
|
docs: RULE_DOCS["c7-seeds-sql-only"],
|
|
2100
|
-
messages: { notSql: "
|
|
2249
|
+
messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/10-linting.md)." },
|
|
2101
2250
|
type: "problem"
|
|
2102
2251
|
}
|
|
2103
2252
|
};
|
|
2104
2253
|
//#endregion
|
|
2105
2254
|
//#region src/lint/rules/c8-referenced-fixture-exists.ts
|
|
2106
2255
|
const TEST_FILE$6 = /\.test\.[cm]?[jt]sx?$/;
|
|
2107
|
-
/** The
|
|
2256
|
+
/** The ground directory each fixture-referencing verb reads from. */
|
|
2108
2257
|
const VERB_ROOTS = {
|
|
2109
|
-
request:
|
|
2110
|
-
seed:
|
|
2111
|
-
toMatch:
|
|
2258
|
+
request: GROUND_REQUESTS,
|
|
2259
|
+
seed: GROUND_SEEDS,
|
|
2260
|
+
toMatch: GROUND_EXPECTED
|
|
2112
2261
|
};
|
|
2113
2262
|
/**
|
|
2114
2263
|
* A fixture reference is a filename/path, never inline content. `.seed()` also
|
|
@@ -2120,15 +2269,15 @@ function looksLikePath(value) {
|
|
|
2120
2269
|
}
|
|
2121
2270
|
/** Resolve the on-disk target a `.fixture()` literal points at, if determinable. */
|
|
2122
2271
|
function resolveFixture(argument, featureDir, specsDir) {
|
|
2123
|
-
if (argument.startsWith("$FIXTURES/")) return join(specsDir,
|
|
2272
|
+
if (argument.startsWith("$FIXTURES/")) return join(specsDir, GROUND_FIXTURES, argument.slice(10));
|
|
2124
2273
|
if (argument.startsWith("$")) return;
|
|
2125
|
-
return join(featureDir,
|
|
2274
|
+
return join(featureDir, GROUND_FIXTURES, argument);
|
|
2126
2275
|
}
|
|
2127
2276
|
/**
|
|
2128
2277
|
* CONVENTIONS C8 — a fixture referenced by a literal must exist on disk under
|
|
2129
|
-
* its
|
|
2130
|
-
* `.fixture(x)`→
|
|
2131
|
-
* `specs/
|
|
2278
|
+
* its ground root: `.request(x)`→`_requests/x`, `.seed(x)`→`_seeds/x`,
|
|
2279
|
+
* `.fixture(x)`→the leaf's `_fixtures/` (or the `$FIXTURES/` pool at the nearest
|
|
2280
|
+
* `specs/_fixtures/`), `toMatch(x)`→`_expected/x` (file or tree snapshot). A typo
|
|
2132
2281
|
* that would fail only at runtime is caught statically. Non-literal arguments
|
|
2133
2282
|
* are out of static reach and skipped.
|
|
2134
2283
|
*/
|
|
@@ -2150,7 +2299,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2150
2299
|
if (target !== void 0 && !isFile(target) && !isDirectory(target)) context.report({
|
|
2151
2300
|
data: {
|
|
2152
2301
|
path: value,
|
|
2153
|
-
root:
|
|
2302
|
+
root: GROUND_FIXTURES
|
|
2154
2303
|
},
|
|
2155
2304
|
messageId: "missing",
|
|
2156
2305
|
node: args[0]
|
|
@@ -2174,7 +2323,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2174
2323
|
},
|
|
2175
2324
|
meta: {
|
|
2176
2325
|
docs: RULE_DOCS["c8-referenced-fixture-exists"],
|
|
2177
|
-
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its
|
|
2326
|
+
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
|
|
2178
2327
|
type: "problem"
|
|
2179
2328
|
}
|
|
2180
2329
|
};
|
|
@@ -2588,7 +2737,7 @@ const d6wTransformTokenEquivalent = {
|
|
|
2588
2737
|
},
|
|
2589
2738
|
meta: {
|
|
2590
2739
|
docs: RULE_DOCS["d6w-transform-token-equivalent"],
|
|
2591
|
-
messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the
|
|
2740
|
+
messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the _expected/ fixture instead; transform is an escape hatch for uncovered noise (D6 — see docs/10-linting.md)." },
|
|
2592
2741
|
type: "suggestion"
|
|
2593
2742
|
}
|
|
2594
2743
|
};
|
|
@@ -2657,7 +2806,7 @@ function refsInFixture(target) {
|
|
|
2657
2806
|
* `{{uuid#order}}`) earns its keep only when it is asserted more than once (the
|
|
2658
2807
|
* point of a capture is the equality check on later occurrences). Aggregating
|
|
2659
2808
|
* per file — code `match.ref` literals plus the `{{kind#ref}}` tokens of the
|
|
2660
|
-
* `
|
|
2809
|
+
* `_expected/` fixtures the file references — a ref that appears exactly once
|
|
2661
2810
|
* everywhere is a plain matcher wearing a name.
|
|
2662
2811
|
*/
|
|
2663
2812
|
const d9wSingleUseRef = {
|
|
@@ -2685,7 +2834,7 @@ const d9wSingleUseRef = {
|
|
|
2685
2834
|
if (memberPropertyName(node.callee) === "toMatch") {
|
|
2686
2835
|
const name = stringValue(node.arguments?.[0]);
|
|
2687
2836
|
if (name !== void 0) {
|
|
2688
|
-
const target = join(featureDir,
|
|
2837
|
+
const target = join(featureDir, GROUND_EXPECTED, name.replace(/\/+$/, ""));
|
|
2689
2838
|
for (const ref of refsInFixture(target)) {
|
|
2690
2839
|
bump(ref);
|
|
2691
2840
|
if (!fixtureNodes.has(ref)) fixtureNodes.set(ref, node);
|
|
@@ -3539,6 +3688,7 @@ const plugin = {
|
|
|
3539
3688
|
"c1-domain-structure": c1DomainStructure,
|
|
3540
3689
|
"c10-contracts-boundary": c10ContractsBoundary,
|
|
3541
3690
|
"c11-contract-data-pairing": c11ContractDataPairing,
|
|
3691
|
+
"c13-underscored-ground": c13UnderscoredGround,
|
|
3542
3692
|
"c2-http-only-requests": c2HttpOnlyRequests,
|
|
3543
3693
|
"c4-contract-shape": c4ContractShape,
|
|
3544
3694
|
"c6-tomatch-extension": c6ToMatchExtension,
|
package/dist/spec-document.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LineCounter, Scalar, Scalar as Scalar$1, isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
1
|
+
import { LineCounter, Scalar, Scalar as Scalar$1, isMap, isScalar, isSeq, parseDocument, visit } from "yaml";
|
|
2
2
|
//#region src/integrations/yaml/document.ts
|
|
3
3
|
/** A line that opens a mapping key — ` key:` or ` - key:`, never block-scalar text. */
|
|
4
4
|
const KEY_LINE = /^(?<indent> +)(?:- )?[A-Za-z_$'"][^\n]*:/;
|
|
@@ -65,7 +65,8 @@ function parseYamlSource(text) {
|
|
|
65
65
|
lineCounter
|
|
66
66
|
}),
|
|
67
67
|
indent: detectIndent(text),
|
|
68
|
-
lineAt: (offset) => lineCounter.linePos(offset).line
|
|
68
|
+
lineAt: (offset) => lineCounter.linePos(offset).line,
|
|
69
|
+
text
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
/** The YAML parser's own errors, with their line — an empty list when it parsed. */
|
|
@@ -88,16 +89,94 @@ function blockScalar(text) {
|
|
|
88
89
|
scalar.type = Scalar.BLOCK_LITERAL;
|
|
89
90
|
return scalar;
|
|
90
91
|
}
|
|
92
|
+
/** The first offset of the line `offset` sits on. */
|
|
93
|
+
function lineStart(text, offset) {
|
|
94
|
+
return text.lastIndexOf("\n", offset - 1) + 1;
|
|
95
|
+
}
|
|
96
|
+
/** Back over the whitespace before `offset`, to just past the `:` or `-` that owns it. */
|
|
97
|
+
function afterOwner(text, offset) {
|
|
98
|
+
let at = offset;
|
|
99
|
+
while (at > 0 && /\s/.test(text[at - 1])) at -= 1;
|
|
100
|
+
return at;
|
|
101
|
+
}
|
|
102
|
+
/** How many spaces a line opens with. */
|
|
103
|
+
function indentOf(text, offset) {
|
|
104
|
+
const start = lineStart(text, offset);
|
|
105
|
+
return /^ */.exec(text.slice(start))[0].length;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Every OUTERMOST flow collection of a document, in source order. A nested one
|
|
109
|
+
* is part of its parent's text and is restored with it, so descending past a
|
|
110
|
+
* flow collection would splice the same span twice.
|
|
111
|
+
*/
|
|
112
|
+
function flowCollections(text) {
|
|
113
|
+
const found = [];
|
|
114
|
+
const take = (node) => {
|
|
115
|
+
if (node.flow !== true || !node.range) return;
|
|
116
|
+
const from = afterOwner(text, node.range[0]);
|
|
117
|
+
found.push({
|
|
118
|
+
ownerIndent: indentOf(text, from === 0 ? 0 : from - 1),
|
|
119
|
+
range: [from, node.range[1]],
|
|
120
|
+
text: text.slice(from, node.range[1]),
|
|
121
|
+
value: JSON.stringify(node.toJSON())
|
|
122
|
+
});
|
|
123
|
+
return visit.SKIP;
|
|
124
|
+
};
|
|
125
|
+
visit(parseDocument(text), {
|
|
126
|
+
Map: (_key, node) => take(node),
|
|
127
|
+
Seq: (_key, node) => take(node)
|
|
128
|
+
});
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Give every flow collection the rewrite did not change back exactly as it was
|
|
133
|
+
* written — padding, quotes and line breaks included.
|
|
134
|
+
*
|
|
135
|
+
* The `yaml` writer has ONE house style for `{ … }` and `[ … ]`, and it is not
|
|
136
|
+
* the repository formatter's: oxfmt pads a flow mapping and not a flow sequence,
|
|
137
|
+
* and explodes a long one over several lines, while the writer emits `[ 'a' ]`
|
|
138
|
+
* on one line whatever its width. Re-emitting an untouched collection therefore
|
|
139
|
+
* hands the formatter something to undo, and `check --fix` and `oxfmt` rewrite
|
|
140
|
+
* each other for ever. A collection nobody touched belongs to its author, not to
|
|
141
|
+
* either tool.
|
|
142
|
+
*
|
|
143
|
+
* A rendered collection is matched to a source one by VALUE and by the
|
|
144
|
+
* INDENTATION of the line that owns it: the same content under the same key is
|
|
145
|
+
* the same collection, and the indentation guard is what keeps the inner lines
|
|
146
|
+
* of a multi-line spelling true where a fix moved that key. Anything unmatched
|
|
147
|
+
* — added, removed, edited — keeps the writer's rendering.
|
|
148
|
+
*/
|
|
149
|
+
function restoreUntouchedFlow(rendered, text) {
|
|
150
|
+
const sources = flowCollections(text);
|
|
151
|
+
if (sources.length === 0) return rendered;
|
|
152
|
+
const taken = /* @__PURE__ */ new Set();
|
|
153
|
+
const splices = [];
|
|
154
|
+
for (const target of flowCollections(rendered)) {
|
|
155
|
+
const at = sources.findIndex((candidate, index) => !taken.has(index) && candidate.value === target.value && candidate.ownerIndent === target.ownerIndent);
|
|
156
|
+
if (at === -1) continue;
|
|
157
|
+
taken.add(at);
|
|
158
|
+
splices.push({
|
|
159
|
+
range: target.range,
|
|
160
|
+
text: sources[at].text
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
let out = rendered;
|
|
164
|
+
for (const splice of splices.toReversed()) out = out.slice(0, splice.range[0]) + splice.text + out.slice(splice.range[1]);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
91
167
|
/**
|
|
92
168
|
* Render a document back to text, in the indentation it was read with.
|
|
93
169
|
* `lineWidth: 0` disables the folding of long lines — a golden is compared byte
|
|
94
170
|
* for byte, so nothing may decide on its own where to break one.
|
|
171
|
+
*
|
|
172
|
+
* What the caller did not change comes back as it was written — see
|
|
173
|
+
* {@link restoreUntouchedFlow} for the one construct the writer restyles.
|
|
95
174
|
*/
|
|
96
175
|
function renderYamlSource(source) {
|
|
97
|
-
return source.document.toString({
|
|
176
|
+
return restoreUntouchedFlow(source.document.toString({
|
|
98
177
|
indent: source.indent,
|
|
99
178
|
lineWidth: 0
|
|
100
|
-
});
|
|
179
|
+
}), source.text);
|
|
101
180
|
}
|
|
102
181
|
//#endregion
|
|
103
182
|
//#region src/core/literate/spec-document.ts
|
|
@@ -385,7 +464,7 @@ function readSpecFile(content, fileName) {
|
|
|
385
464
|
/**
|
|
386
465
|
* Every text of a document the `{{token}}` grammar governs: the streams a
|
|
387
466
|
* comparison judges, and the `files:` texts it judges them with. `stdin:` is an
|
|
388
|
-
* INPUT — like a `
|
|
467
|
+
* INPUT — like a `_requests/` fixture, it is never matched — and a description or
|
|
389
468
|
* a command is prose, so neither appears here.
|
|
390
469
|
*/
|
|
391
470
|
function assertedStreams(document) {
|
|
@@ -543,7 +622,7 @@ const SPEC_SCHEMA = {
|
|
|
543
622
|
},
|
|
544
623
|
fixture: {
|
|
545
624
|
...STRING_OR_LIST,
|
|
546
|
-
description: "Fixture paths copied into the working directory, layering in order."
|
|
625
|
+
description: "Fixture paths copied into the working directory, layering in order. `$FIXTURES/<name>` is the shared pool at `<specs-root>/_fixtures/`; a bare path is the ground beside this document, at `_fixtures/<name>`."
|
|
547
626
|
},
|
|
548
627
|
kind: {
|
|
549
628
|
default: "cli",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/test",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.1",
|
|
4
4
|
"description": "Declarative testing framework for HTTP APIs, CLIs, and background jobs — one entry point, real infrastructure, golden-file assertions, plus an oxlint convention plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-testing",
|
package/schema/spec.schema.json
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"type": "array"
|
|
137
137
|
}
|
|
138
138
|
],
|
|
139
|
-
"description": "Fixture paths copied into the working directory, layering in order."
|
|
139
|
+
"description": "Fixture paths copied into the working directory, layering in order. `$FIXTURES/<name>` is the shared pool at `<specs-root>/_fixtures/`; a bare path is the ground beside this document, at `_fixtures/<name>`."
|
|
140
140
|
},
|
|
141
141
|
"kind": {
|
|
142
142
|
"default": "cli",
|