@jterrazz/test 14.2.0 → 15.0.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 +10 -10
- package/dist/ambiguity.js +1 -1
- package/dist/appium.adapter.js +1 -1
- package/dist/artifacts.js +19 -7
- package/dist/checker.js +14 -14
- package/dist/oxlint.cjs +90 -281
- package/dist/oxlint.d.cts +2 -2
- package/dist/oxlint.d.ts +2 -2
- package/dist/oxlint.js +90 -281
- package/package.json +3 -2
package/dist/oxlint.cjs
CHANGED
|
@@ -198,21 +198,10 @@ function isTestCallee(callee) {
|
|
|
198
198
|
if (callee.type === "CallExpression") return isTestCallee(callee.callee);
|
|
199
199
|
return false;
|
|
200
200
|
}
|
|
201
|
-
/** Does the test callee chain carry a modifier member with the given name? */
|
|
202
|
-
function testCalleeHasModifier(callee, modifier) {
|
|
203
|
-
if (callee === void 0) return false;
|
|
204
|
-
if (callee.type === "MemberExpression") return memberPropertyName(callee) === modifier || testCalleeHasModifier(callee.object, modifier);
|
|
205
|
-
if (callee.type === "CallExpression") return testCalleeHasModifier(callee.callee, modifier);
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
201
|
/** The callback argument of a test call (arrow or function expression), if any. */
|
|
209
202
|
function findTestCallback(args) {
|
|
210
203
|
return args.find((arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
|
|
211
204
|
}
|
|
212
|
-
/** The static string name of a test call (`test('name', …)`), if a literal. */
|
|
213
|
-
function testName(callNode) {
|
|
214
|
-
return stringValue((callNode.arguments ?? [])[0]);
|
|
215
|
-
}
|
|
216
205
|
/**
|
|
217
206
|
* Visitor fragment collecting every import-like source: static imports,
|
|
218
207
|
* dynamic `import()`, and re-exports. Rules spread this into their visitor.
|
|
@@ -239,6 +228,26 @@ function importSourceVisitor(onSource) {
|
|
|
239
228
|
* The **statique** channel — one entry per shipped `jterrazz/*` rule. Each rule
|
|
240
229
|
* file attaches its entry as `meta.docs`, so a rule and its normative text can
|
|
241
230
|
* never drift (the completeness meta-test asserts every rule carries one).
|
|
231
|
+
*
|
|
232
|
+
* **What this channel does NOT own.** A convention oxlint's own `vitest` plugin
|
|
233
|
+
* already enforces belongs to that plugin, not here — `@jterrazz/typescript` v10
|
|
234
|
+
* turns the plugin on over the test globs in every profile, so a consumer of the
|
|
235
|
+
* preset gets it without wiring. Four hygiene rules left the channel on that
|
|
236
|
+
* ground (J1, J3, J4, J5 → `vitest/no-focused-tests` + `vitest/no-disabled-tests`,
|
|
237
|
+
* `vitest/expect-expect`, `vitest/no-identical-title`, `vitest/prefer-lowercase-title`).
|
|
238
|
+
* `prefer-lowercase-title` is stricter than J5 was: a title opening on an all-caps
|
|
239
|
+
* identifier (`HTTP 404 …`, `DI …`) now fails, and the rulebook carries no
|
|
240
|
+
* `allowedPrefixes` — a title starts lowercase, whatever its first word. The
|
|
241
|
+
* rest are behaviour-identical on this package's own fixtures. Only J2 (the
|
|
242
|
+
* arbitrary-sleep ban, which has no upstream counterpart) stays statique.
|
|
243
|
+
*
|
|
244
|
+
* **Severity.** The estate's rulebook has no warn tier: a rule is on at `error`
|
|
245
|
+
* or off with a recorded reason, and that law binds `@jterrazz/typescript`'s
|
|
246
|
+
* rulebook. This channel is its one documented exception, and it is narrow — an
|
|
247
|
+
* id shaped `<family><n>w-…` is a REDUNDANCY heuristic (a `root` the walk would
|
|
248
|
+
* have found, a probe cluster that wants a golden), a reading of taste that
|
|
249
|
+
* would block a legitimate shape if it failed the build. Every hard convention
|
|
250
|
+
* here is an `error`; `plugin.test.ts` holds the split.
|
|
242
251
|
*/
|
|
243
252
|
const RULE_DOCS = {
|
|
244
253
|
"a1-specification-file": {
|
|
@@ -514,13 +523,6 @@ const RULE_DOCS = {
|
|
|
514
523
|
id: "I4",
|
|
515
524
|
rationale: "Dans les tests de module, mocks et données sont du CODE (`mockOf`, `*.fixtures.ts`) ; un vrai fichier appelle une spec."
|
|
516
525
|
},
|
|
517
|
-
"j1-no-only-skip": {
|
|
518
|
-
channel: "statique",
|
|
519
|
-
convention: "Aucun `.only` / `.skip` committé (`describe.only`, `test.only`, `test.skip`).",
|
|
520
|
-
family: "J",
|
|
521
|
-
id: "J1",
|
|
522
|
-
rationale: "Un `.only`/`.skip` oublié désactive silencieusement une partie de la suite."
|
|
523
|
-
},
|
|
524
526
|
"j2-no-sleep-in-specs": {
|
|
525
527
|
channel: "statique",
|
|
526
528
|
convention: "Aucun sleep arbitraire (`setTimeout`/`setInterval`/`Atomics.wait`) sous `specs/**` — la synchronisation passe par `waitFor`.",
|
|
@@ -528,27 +530,6 @@ const RULE_DOCS = {
|
|
|
528
530
|
id: "J2",
|
|
529
531
|
rationale: "Un sleep fixe rend les tests lents et instables ; attendre une condition est déterministe."
|
|
530
532
|
},
|
|
531
|
-
"j3-no-expectless-test": {
|
|
532
|
-
channel: "statique",
|
|
533
|
-
convention: "Un `test(...)` avec callback contient au moins un `expect(…)` ; `test.todo` (sans callback) est ignoré.",
|
|
534
|
-
family: "J",
|
|
535
|
-
id: "J3",
|
|
536
|
-
rationale: "Un test sans assertion est mort ou muet — il passe toujours sans rien vérifier."
|
|
537
|
-
},
|
|
538
|
-
"j4-unique-test-names": {
|
|
539
|
-
channel: "statique",
|
|
540
|
-
convention: "Deux tests d’un même fichier ne partagent pas un nom littéral (`.each` ignoré).",
|
|
541
|
-
family: "J",
|
|
542
|
-
id: "J4",
|
|
543
|
-
rationale: "Le nom du test est son unique description — deux noms identiques rendent un échec ambigu."
|
|
544
|
-
},
|
|
545
|
-
"j5-lowercase-title": {
|
|
546
|
-
channel: "statique",
|
|
547
|
-
convention: "La première lettre d’un titre `test()`/`describe()`/`it()` littéral est en minuscule. Exemptés : les titres dont le premier MOT est un identifiant tout en majuscules/underscores (`VALID_CATEGORIES`, `HTTP`, `DI`) et ceux démarrant sur un non-lettre — seuls les premiers mots de prose minusculisables sont contraints.",
|
|
548
|
-
family: "J",
|
|
549
|
-
id: "J5",
|
|
550
|
-
rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
|
|
551
|
-
},
|
|
552
533
|
"w1-scenario-pure": {
|
|
553
534
|
channel: "statique",
|
|
554
535
|
convention: "Un scénario (`.visit()` website, `.open()` mobile) est le When : le visiteur agit, la capture reflète l’état final ; aucun `expect()` dans le callback — les assertions vivent dans le Then, sur le résultat retourné.",
|
|
@@ -907,7 +888,7 @@ const a1SpecificationFile = {
|
|
|
907
888
|
},
|
|
908
889
|
meta: {
|
|
909
890
|
docs: RULE_DOCS["a1-specification-file"],
|
|
910
|
-
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/
|
|
891
|
+
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/13-linting.md)." },
|
|
911
892
|
type: "problem"
|
|
912
893
|
}
|
|
913
894
|
};
|
|
@@ -984,13 +965,13 @@ const a10DuplicateBinding = {
|
|
|
984
965
|
},
|
|
985
966
|
meta: {
|
|
986
967
|
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
987
|
-
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/
|
|
968
|
+
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/13-linting.md)." },
|
|
988
969
|
type: "problem"
|
|
989
970
|
}
|
|
990
971
|
};
|
|
991
972
|
//#endregion
|
|
992
973
|
//#region src/lint/rules/a2-known-constructors.ts
|
|
993
|
-
/** The five constructors, and only five (A2 — see docs/
|
|
974
|
+
/** The five constructors, and only five (A2 — see docs/13-linting.md). */
|
|
994
975
|
const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
995
976
|
"api",
|
|
996
977
|
"cli",
|
|
@@ -1019,7 +1000,7 @@ const a2KnownConstructors = {
|
|
|
1019
1000
|
},
|
|
1020
1001
|
meta: {
|
|
1021
1002
|
docs: RULE_DOCS["a2-known-constructors"],
|
|
1022
|
-
messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/
|
|
1003
|
+
messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
|
|
1023
1004
|
type: "problem"
|
|
1024
1005
|
}
|
|
1025
1006
|
};
|
|
@@ -1064,7 +1045,7 @@ const a3NoDestructureAlias = {
|
|
|
1064
1045
|
},
|
|
1065
1046
|
meta: {
|
|
1066
1047
|
docs: RULE_DOCS["a3-no-destructure-alias"],
|
|
1067
|
-
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/
|
|
1048
|
+
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/13-linting.md). Rename at the import site if a different local name is needed." },
|
|
1068
1049
|
type: "problem"
|
|
1069
1050
|
}
|
|
1070
1051
|
};
|
|
@@ -1116,7 +1097,7 @@ const a4CleanupAfterall = {
|
|
|
1116
1097
|
},
|
|
1117
1098
|
meta: {
|
|
1118
1099
|
docs: RULE_DOCS["a4-cleanup-afterall"],
|
|
1119
|
-
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/
|
|
1100
|
+
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/13-linting.md)." },
|
|
1120
1101
|
type: "problem"
|
|
1121
1102
|
}
|
|
1122
1103
|
};
|
|
@@ -1146,7 +1127,7 @@ const a5ModeWithServer = {
|
|
|
1146
1127
|
},
|
|
1147
1128
|
meta: {
|
|
1148
1129
|
docs: RULE_DOCS["a5-mode-with-server"],
|
|
1149
|
-
messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (A5 — see docs/
|
|
1130
|
+
messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (A5 — see docs/13-linting.md)." },
|
|
1150
1131
|
type: "problem"
|
|
1151
1132
|
}
|
|
1152
1133
|
};
|
|
@@ -1187,7 +1168,7 @@ const a6wRedundantComposeService = {
|
|
|
1187
1168
|
},
|
|
1188
1169
|
meta: {
|
|
1189
1170
|
docs: RULE_DOCS["a6w-redundant-compose-service"],
|
|
1190
|
-
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/
|
|
1171
|
+
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/13-linting.md)." },
|
|
1191
1172
|
type: "suggestion"
|
|
1192
1173
|
}
|
|
1193
1174
|
};
|
|
@@ -1222,7 +1203,7 @@ function findRoot(startDir, exists) {
|
|
|
1222
1203
|
//#endregion
|
|
1223
1204
|
//#region src/lint/rules/a9w-redundant-root.ts
|
|
1224
1205
|
/**
|
|
1225
|
-
* The root the convention would derive (A9 — see docs/
|
|
1206
|
+
* The root the convention would derive (A9 — see docs/13-linting.md): the
|
|
1226
1207
|
* framework's own walk-up, run against this layer's cached fs probe. The rule
|
|
1227
1208
|
* flags a `root` that the runner would have found by itself, so the two MUST
|
|
1228
1209
|
* be the same function — a private copy of the walk drifted from it before.
|
|
@@ -1254,7 +1235,7 @@ const a9wRedundantRoot = {
|
|
|
1254
1235
|
},
|
|
1255
1236
|
meta: {
|
|
1256
1237
|
docs: RULE_DOCS["a9w-redundant-root"],
|
|
1257
|
-
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/
|
|
1238
|
+
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/13-linting.md)." },
|
|
1258
1239
|
type: "suggestion"
|
|
1259
1240
|
}
|
|
1260
1241
|
};
|
|
@@ -1360,7 +1341,7 @@ const b2KnownFixtureMarker = {
|
|
|
1360
1341
|
},
|
|
1361
1342
|
meta: {
|
|
1362
1343
|
docs: RULE_DOCS["b2-known-fixture-marker"],
|
|
1363
|
-
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/
|
|
1344
|
+
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/13-linting.md)." },
|
|
1364
1345
|
type: "problem"
|
|
1365
1346
|
}
|
|
1366
1347
|
};
|
|
@@ -1425,8 +1406,8 @@ const b4GivenThen = {
|
|
|
1425
1406
|
meta: {
|
|
1426
1407
|
docs: RULE_DOCS["b4-given-then"],
|
|
1427
1408
|
messages: {
|
|
1428
|
-
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/
|
|
1429
|
-
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/
|
|
1409
|
+
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/13-linting.md).",
|
|
1410
|
+
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/13-linting.md — every test needs both `// Given -` and `// Then -`)."
|
|
1430
1411
|
},
|
|
1431
1412
|
type: "problem"
|
|
1432
1413
|
}
|
|
@@ -1467,7 +1448,7 @@ const b5AwaitUsing = {
|
|
|
1467
1448
|
meta: {
|
|
1468
1449
|
docs: RULE_DOCS["b5-await-using"],
|
|
1469
1450
|
defaultOptions: [{ runners: [] }],
|
|
1470
|
-
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/
|
|
1451
|
+
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/13-linting.md)." },
|
|
1471
1452
|
schema: [{
|
|
1472
1453
|
additionalProperties: false,
|
|
1473
1454
|
properties: { runners: {
|
|
@@ -1510,7 +1491,7 @@ const b6wRedundantEnvUrl = {
|
|
|
1510
1491
|
},
|
|
1511
1492
|
meta: {
|
|
1512
1493
|
docs: RULE_DOCS["b6w-redundant-env-url"],
|
|
1513
|
-
messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (B6 — see docs/
|
|
1494
|
+
messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (B6 — see docs/13-linting.md)." },
|
|
1514
1495
|
type: "suggestion"
|
|
1515
1496
|
}
|
|
1516
1497
|
};
|
|
@@ -1539,7 +1520,7 @@ const b8KebabTrigger = {
|
|
|
1539
1520
|
},
|
|
1540
1521
|
meta: {
|
|
1541
1522
|
docs: RULE_DOCS["b8-kebab-trigger"],
|
|
1542
|
-
messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (B8 — see docs/
|
|
1523
|
+
messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (B8 — see docs/13-linting.md)." },
|
|
1543
1524
|
type: "problem"
|
|
1544
1525
|
}
|
|
1545
1526
|
};
|
|
@@ -1594,7 +1575,7 @@ const b9wProductCommand = {
|
|
|
1594
1575
|
},
|
|
1595
1576
|
meta: {
|
|
1596
1577
|
docs: RULE_DOCS["b9w-product-command"],
|
|
1597
|
-
messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (B9 — see docs/
|
|
1578
|
+
messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (B9 — see docs/13-linting.md). Suppress with a reason if the product genuinely is this binary." },
|
|
1598
1579
|
type: "suggestion"
|
|
1599
1580
|
}
|
|
1600
1581
|
};
|
|
@@ -1747,14 +1728,14 @@ const c1DomainStructure = {
|
|
|
1747
1728
|
defaultOptions: [{ depth: DEFAULT_DEPTH }],
|
|
1748
1729
|
docs: RULE_DOCS["c1-domain-structure"],
|
|
1749
1730
|
messages: {
|
|
1750
|
-
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/
|
|
1751
|
-
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/
|
|
1752
|
-
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/
|
|
1753
|
-
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/
|
|
1754
|
-
testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/
|
|
1755
|
-
testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/
|
|
1756
|
-
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/
|
|
1757
|
-
testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/
|
|
1731
|
+
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/13-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.",
|
|
1732
|
+
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/13-linting.md).",
|
|
1733
|
+
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/13-linting.md).",
|
|
1734
|
+
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/13-linting.md).",
|
|
1735
|
+
testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/13-linting.md).",
|
|
1736
|
+
testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/13-linting.md).",
|
|
1737
|
+
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/13-linting.md).",
|
|
1738
|
+
testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/13-linting.md)."
|
|
1758
1739
|
},
|
|
1759
1740
|
schema: [{
|
|
1760
1741
|
additionalProperties: false,
|
|
@@ -1799,7 +1780,7 @@ const c10ContractsBoundary = {
|
|
|
1799
1780
|
},
|
|
1800
1781
|
meta: {
|
|
1801
1782
|
docs: RULE_DOCS["c10-contracts-boundary"],
|
|
1802
|
-
messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/
|
|
1783
|
+
messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/13-linting.md)." },
|
|
1803
1784
|
type: "problem"
|
|
1804
1785
|
}
|
|
1805
1786
|
};
|
|
@@ -1854,7 +1835,7 @@ const c11ContractDataPairing = {
|
|
|
1854
1835
|
},
|
|
1855
1836
|
meta: {
|
|
1856
1837
|
docs: RULE_DOCS["c11-contract-data-pairing"],
|
|
1857
|
-
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/
|
|
1838
|
+
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/13-linting.md)." },
|
|
1858
1839
|
type: "problem"
|
|
1859
1840
|
}
|
|
1860
1841
|
};
|
|
@@ -1907,7 +1888,7 @@ const c13UnderscoredGround = {
|
|
|
1907
1888
|
},
|
|
1908
1889
|
meta: {
|
|
1909
1890
|
docs: RULE_DOCS["c13-underscored-ground"],
|
|
1910
|
-
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/
|
|
1891
|
+
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/13-linting.md)." },
|
|
1911
1892
|
type: "problem"
|
|
1912
1893
|
}
|
|
1913
1894
|
};
|
|
@@ -1936,7 +1917,7 @@ const c2HttpOnlyRequests = {
|
|
|
1936
1917
|
},
|
|
1937
1918
|
meta: {
|
|
1938
1919
|
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1939
|
-
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/
|
|
1920
|
+
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
|
|
1940
1921
|
type: "problem"
|
|
1941
1922
|
}
|
|
1942
1923
|
};
|
|
@@ -2160,18 +2141,18 @@ const c4ContractShape = {
|
|
|
2160
2141
|
meta: {
|
|
2161
2142
|
docs: RULE_DOCS["c4-contract-shape"],
|
|
2162
2143
|
messages: {
|
|
2163
|
-
badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/
|
|
2164
|
-
badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/
|
|
2165
|
-
missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/
|
|
2166
|
-
missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/
|
|
2167
|
-
notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/
|
|
2168
|
-
notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/
|
|
2169
|
-
providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/
|
|
2170
|
-
providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/
|
|
2171
|
-
providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/
|
|
2172
|
-
rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/
|
|
2173
|
-
rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/
|
|
2174
|
-
tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/
|
|
2144
|
+
badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/13-linting.md).",
|
|
2145
|
+
badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/13-linting.md).",
|
|
2146
|
+
missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/13-linting.md).",
|
|
2147
|
+
missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/13-linting.md).",
|
|
2148
|
+
notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/13-linting.md).",
|
|
2149
|
+
notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/13-linting.md).",
|
|
2150
|
+
providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/13-linting.md).",
|
|
2151
|
+
providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/13-linting.md).",
|
|
2152
|
+
providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/13-linting.md).",
|
|
2153
|
+
rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/13-linting.md).",
|
|
2154
|
+
rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/13-linting.md).",
|
|
2155
|
+
tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/13-linting.md)."
|
|
2175
2156
|
},
|
|
2176
2157
|
type: "problem"
|
|
2177
2158
|
}
|
|
@@ -2222,7 +2203,7 @@ const c6ToMatchExtension = {
|
|
|
2222
2203
|
},
|
|
2223
2204
|
meta: {
|
|
2224
2205
|
docs: RULE_DOCS["c6-tomatch-extension"],
|
|
2225
|
-
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/
|
|
2206
|
+
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/13-linting.md)." },
|
|
2226
2207
|
type: "problem"
|
|
2227
2208
|
}
|
|
2228
2209
|
};
|
|
@@ -2250,7 +2231,7 @@ const c7SeedsSqlOnly = {
|
|
|
2250
2231
|
},
|
|
2251
2232
|
meta: {
|
|
2252
2233
|
docs: RULE_DOCS["c7-seeds-sql-only"],
|
|
2253
|
-
messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/
|
|
2234
|
+
messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/13-linting.md)." },
|
|
2254
2235
|
type: "problem"
|
|
2255
2236
|
}
|
|
2256
2237
|
};
|
|
@@ -2327,7 +2308,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2327
2308
|
},
|
|
2328
2309
|
meta: {
|
|
2329
2310
|
docs: RULE_DOCS["c8-referenced-fixture-exists"],
|
|
2330
|
-
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/
|
|
2311
|
+
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/13-linting.md). Create it or fix the reference." },
|
|
2331
2312
|
type: "problem"
|
|
2332
2313
|
}
|
|
2333
2314
|
};
|
|
@@ -2396,7 +2377,7 @@ const d12wResponseBodyProbe = {
|
|
|
2396
2377
|
meta: {
|
|
2397
2378
|
docs: RULE_DOCS["d12w-response-body-probe"],
|
|
2398
2379
|
defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
|
|
2399
|
-
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/
|
|
2380
|
+
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/13-linting.md)." },
|
|
2400
2381
|
schema: [{
|
|
2401
2382
|
additionalProperties: false,
|
|
2402
2383
|
properties: { threshold: {
|
|
@@ -2479,7 +2460,7 @@ const d13wUnfrozenNegativeFixture = {
|
|
|
2479
2460
|
},
|
|
2480
2461
|
meta: {
|
|
2481
2462
|
docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
|
|
2482
|
-
messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/
|
|
2463
|
+
messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/13-linting.md)." },
|
|
2483
2464
|
type: "suggestion"
|
|
2484
2465
|
}
|
|
2485
2466
|
};
|
|
@@ -2555,7 +2536,7 @@ const d15wStatusOnlyProbe = {
|
|
|
2555
2536
|
},
|
|
2556
2537
|
meta: {
|
|
2557
2538
|
docs: RULE_DOCS["d15w-status-only-probe"],
|
|
2558
|
-
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/
|
|
2539
|
+
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/13-linting.md)." },
|
|
2559
2540
|
type: "suggestion"
|
|
2560
2541
|
}
|
|
2561
2542
|
};
|
|
@@ -2610,7 +2591,7 @@ const d2AwaitIoMatcher = {
|
|
|
2610
2591
|
},
|
|
2611
2592
|
meta: {
|
|
2612
2593
|
docs: RULE_DOCS["d2-await-io-matcher"],
|
|
2613
|
-
messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (D2 — see docs/
|
|
2594
|
+
messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (D2 — see docs/13-linting.md)." },
|
|
2614
2595
|
type: "problem"
|
|
2615
2596
|
}
|
|
2616
2597
|
};
|
|
@@ -2664,7 +2645,7 @@ const d2wAwaitSyncMatcher = {
|
|
|
2664
2645
|
},
|
|
2665
2646
|
meta: {
|
|
2666
2647
|
docs: RULE_DOCS["d2w-await-sync-matcher"],
|
|
2667
|
-
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/
|
|
2648
|
+
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/13-linting.md)." },
|
|
2668
2649
|
type: "suggestion"
|
|
2669
2650
|
}
|
|
2670
2651
|
};
|
|
@@ -2741,7 +2722,7 @@ const d6wTransformTokenEquivalent = {
|
|
|
2741
2722
|
},
|
|
2742
2723
|
meta: {
|
|
2743
2724
|
docs: RULE_DOCS["d6w-transform-token-equivalent"],
|
|
2744
|
-
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/
|
|
2725
|
+
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/13-linting.md)." },
|
|
2745
2726
|
type: "suggestion"
|
|
2746
2727
|
}
|
|
2747
2728
|
};
|
|
@@ -2775,7 +2756,7 @@ const d8wTextBypass = {
|
|
|
2775
2756
|
},
|
|
2776
2757
|
meta: {
|
|
2777
2758
|
docs: RULE_DOCS["d8w-text-bypass"],
|
|
2778
|
-
messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (D8 — see docs/
|
|
2759
|
+
messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (D8 — see docs/13-linting.md)." },
|
|
2779
2760
|
type: "suggestion"
|
|
2780
2761
|
}
|
|
2781
2762
|
};
|
|
@@ -2858,7 +2839,7 @@ const d9wSingleUseRef = {
|
|
|
2858
2839
|
},
|
|
2859
2840
|
meta: {
|
|
2860
2841
|
docs: RULE_DOCS["d9w-single-use-ref"],
|
|
2861
|
-
messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (D9 — see docs/
|
|
2842
|
+
messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (D9 — see docs/13-linting.md)." },
|
|
2862
2843
|
type: "suggestion"
|
|
2863
2844
|
}
|
|
2864
2845
|
};
|
|
@@ -2952,7 +2933,7 @@ const f1NoSubpathImport = {
|
|
|
2952
2933
|
},
|
|
2953
2934
|
meta: {
|
|
2954
2935
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2955
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/
|
|
2936
|
+
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/13-linting.md). Published subpaths, exempt everywhere: {{published}}." },
|
|
2956
2937
|
type: "problem"
|
|
2957
2938
|
}
|
|
2958
2939
|
};
|
|
@@ -3000,7 +2981,7 @@ const f2NoTestImportsInProd = {
|
|
|
3000
2981
|
},
|
|
3001
2982
|
meta: {
|
|
3002
2983
|
docs: RULE_DOCS["f2-no-test-imports-in-prod"],
|
|
3003
|
-
messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (F2 — see docs/
|
|
2984
|
+
messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (F2 — see docs/13-linting.md)." },
|
|
3004
2985
|
type: "problem"
|
|
3005
2986
|
}
|
|
3006
2987
|
};
|
|
@@ -3063,7 +3044,7 @@ const f3SpecsPublicEntry = {
|
|
|
3063
3044
|
},
|
|
3064
3045
|
meta: {
|
|
3065
3046
|
docs: RULE_DOCS["f3-specs-public-entry"],
|
|
3066
|
-
messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/
|
|
3047
|
+
messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
|
|
3067
3048
|
type: "problem"
|
|
3068
3049
|
}
|
|
3069
3050
|
};
|
|
@@ -3089,7 +3070,7 @@ const f4NoTestToTestImport = {
|
|
|
3089
3070
|
},
|
|
3090
3071
|
meta: {
|
|
3091
3072
|
docs: RULE_DOCS["f4-no-test-to-test-import"],
|
|
3092
|
-
messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (F4 — see docs/
|
|
3073
|
+
messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (F4 — see docs/13-linting.md)." },
|
|
3093
3074
|
type: "problem"
|
|
3094
3075
|
}
|
|
3095
3076
|
};
|
|
@@ -3115,7 +3096,7 @@ const f5FixturesOnlyFromTests = {
|
|
|
3115
3096
|
},
|
|
3116
3097
|
meta: {
|
|
3117
3098
|
docs: RULE_DOCS["f5-fixtures-only-from-tests"],
|
|
3118
|
-
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/
|
|
3099
|
+
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/13-linting.md)." },
|
|
3119
3100
|
type: "problem"
|
|
3120
3101
|
}
|
|
3121
3102
|
};
|
|
@@ -3207,8 +3188,8 @@ const i1LayerBoundaries = {
|
|
|
3207
3188
|
defaultOptions: [{ layers: {} }],
|
|
3208
3189
|
docs: RULE_DOCS["i1-layer-boundaries"],
|
|
3209
3190
|
messages: {
|
|
3210
|
-
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/
|
|
3211
|
-
foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/
|
|
3191
|
+
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/13-linting.md).",
|
|
3192
|
+
foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/13-linting.md)."
|
|
3212
3193
|
},
|
|
3213
3194
|
schema: [{
|
|
3214
3195
|
additionalProperties: false,
|
|
@@ -3296,9 +3277,9 @@ const i2SiblingTestNaming = {
|
|
|
3296
3277
|
meta: {
|
|
3297
3278
|
docs: RULE_DOCS["i2-sibling-test-naming"],
|
|
3298
3279
|
messages: {
|
|
3299
|
-
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/
|
|
3300
|
-
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/
|
|
3301
|
-
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/
|
|
3280
|
+
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/13-linting.md). A test needing more than its module is a specification: move it to specs/.",
|
|
3281
|
+
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/13-linting.md).",
|
|
3282
|
+
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/13-linting.md)."
|
|
3302
3283
|
},
|
|
3303
3284
|
type: "problem"
|
|
3304
3285
|
}
|
|
@@ -3398,57 +3379,14 @@ const i4NoViMockInSrc = {
|
|
|
3398
3379
|
meta: {
|
|
3399
3380
|
docs: RULE_DOCS["i4-no-vi-mock-in-src"],
|
|
3400
3381
|
messages: {
|
|
3401
|
-
assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (I4 — see docs/
|
|
3402
|
-
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/
|
|
3403
|
-
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/
|
|
3382
|
+
assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (I4 — see docs/13-linting.md).",
|
|
3383
|
+
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/13-linting.md).",
|
|
3384
|
+
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/13-linting.md)."
|
|
3404
3385
|
},
|
|
3405
3386
|
type: "problem"
|
|
3406
3387
|
}
|
|
3407
3388
|
};
|
|
3408
3389
|
//#endregion
|
|
3409
|
-
//#region src/lint/rules/j1-no-only-skip.ts
|
|
3410
|
-
/** Test runners whose `.only` / `.skip` modifiers must never be committed. */
|
|
3411
|
-
const RUNNERS = /* @__PURE__ */ new Set([
|
|
3412
|
-
"describe",
|
|
3413
|
-
"it",
|
|
3414
|
-
"test"
|
|
3415
|
-
]);
|
|
3416
|
-
/** The focus/skip modifiers CONVENTIONS J1 forbids. */
|
|
3417
|
-
const FORBIDDEN = /* @__PURE__ */ new Set(["only", "skip"]);
|
|
3418
|
-
/**
|
|
3419
|
-
* CONVENTIONS J1 — no committed `.only` / `.skip`.
|
|
3420
|
-
*
|
|
3421
|
-
* Flags the member form used across the specs: `describe.only`, `test.only`,
|
|
3422
|
-
* `it.only` and their `.skip` counterparts. Modifiers that legitimately gate a
|
|
3423
|
-
* test (`test.skipIf`, `test.runIf`) are untouched — only the bare `only`/`skip`
|
|
3424
|
-
* that pin or disable a test in a commit are reported.
|
|
3425
|
-
*/
|
|
3426
|
-
const j1NoOnlySkip = {
|
|
3427
|
-
create(context) {
|
|
3428
|
-
return { MemberExpression(node) {
|
|
3429
|
-
if (node.computed === true) return;
|
|
3430
|
-
const object = node.object;
|
|
3431
|
-
const property = node.property;
|
|
3432
|
-
if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
|
|
3433
|
-
const runner = object.name;
|
|
3434
|
-
const modifier = property.name;
|
|
3435
|
-
if (RUNNERS.has(runner) && FORBIDDEN.has(modifier)) context.report({
|
|
3436
|
-
data: {
|
|
3437
|
-
modifier,
|
|
3438
|
-
runner
|
|
3439
|
-
},
|
|
3440
|
-
messageId: "forbidden",
|
|
3441
|
-
node
|
|
3442
|
-
});
|
|
3443
|
-
} };
|
|
3444
|
-
},
|
|
3445
|
-
meta: {
|
|
3446
|
-
docs: RULE_DOCS["j1-no-only-skip"],
|
|
3447
|
-
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
|
|
3448
|
-
type: "problem"
|
|
3449
|
-
}
|
|
3450
|
-
};
|
|
3451
|
-
//#endregion
|
|
3452
3390
|
//#region src/lint/rules/j2-no-sleep-in-specs.ts
|
|
3453
3391
|
/**
|
|
3454
3392
|
* CONVENTIONS J2 — spec files (`specs/**`) contain no arbitrary sleeps:
|
|
@@ -3487,138 +3425,13 @@ const j2NoSleepInSpecs = {
|
|
|
3487
3425
|
meta: {
|
|
3488
3426
|
docs: RULE_DOCS["j2-no-sleep-in-specs"],
|
|
3489
3427
|
messages: {
|
|
3490
|
-
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/
|
|
3491
|
-
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/
|
|
3428
|
+
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md).",
|
|
3429
|
+
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md)."
|
|
3492
3430
|
},
|
|
3493
3431
|
type: "problem"
|
|
3494
3432
|
}
|
|
3495
3433
|
};
|
|
3496
3434
|
//#endregion
|
|
3497
|
-
//#region src/lint/rules/j3-no-expectless-test.ts
|
|
3498
|
-
/** Does `callback` contain at least one `expect(…)` call? */
|
|
3499
|
-
function hasExpect(callback) {
|
|
3500
|
-
let found = false;
|
|
3501
|
-
walk(callback, (node) => {
|
|
3502
|
-
if (found || node.type !== "CallExpression") return;
|
|
3503
|
-
const callee = node.callee;
|
|
3504
|
-
if (callee?.type === "Identifier" && callee.name === "expect") found = true;
|
|
3505
|
-
});
|
|
3506
|
-
return found;
|
|
3507
|
-
}
|
|
3508
|
-
/**
|
|
3509
|
-
* CONVENTIONS J3 — a test asserts something. A `test('…', () => { … })` whose
|
|
3510
|
-
* body contains no `expect(…)` is either dead or a silent no-op.
|
|
3511
|
-
*
|
|
3512
|
-
* Reuses the shared B4 test-shape helpers. `test.todo(...)` (and any other
|
|
3513
|
-
* callback-less form) is skipped — there is nothing to assert yet.
|
|
3514
|
-
*/
|
|
3515
|
-
const j3NoExpectlessTest = {
|
|
3516
|
-
create(context) {
|
|
3517
|
-
return { CallExpression(node) {
|
|
3518
|
-
const callee = node.callee;
|
|
3519
|
-
if (!isTestCallee(callee)) return;
|
|
3520
|
-
if (testCalleeHasModifier(callee, "todo")) return;
|
|
3521
|
-
const callback = findTestCallback(node.arguments ?? []);
|
|
3522
|
-
if (callback === void 0) return;
|
|
3523
|
-
if (!hasExpect(callback)) context.report({
|
|
3524
|
-
messageId: "noExpect",
|
|
3525
|
-
node
|
|
3526
|
-
});
|
|
3527
|
-
} };
|
|
3528
|
-
},
|
|
3529
|
-
meta: {
|
|
3530
|
-
docs: RULE_DOCS["j3-no-expectless-test"],
|
|
3531
|
-
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
|
|
3532
|
-
type: "problem"
|
|
3533
|
-
}
|
|
3534
|
-
};
|
|
3535
|
-
//#endregion
|
|
3536
|
-
//#region src/lint/rules/j4-unique-test-names.ts
|
|
3537
|
-
/**
|
|
3538
|
-
* CONVENTIONS J4 — test names are the sole description of behaviour (B3), so two
|
|
3539
|
-
* tests sharing a literal name in one file are a copy-paste smell: one shadows
|
|
3540
|
-
* the other in the reporter. Flags the second (and later) occurrence.
|
|
3541
|
-
*
|
|
3542
|
-
* `.each` / parametrized tests are skipped — their name is a template expanded
|
|
3543
|
-
* per row, collisions there are by construction.
|
|
3544
|
-
*/
|
|
3545
|
-
const j4UniqueTestNames = {
|
|
3546
|
-
create(context) {
|
|
3547
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3548
|
-
return { CallExpression(node) {
|
|
3549
|
-
const callee = node.callee;
|
|
3550
|
-
if (!isTestCallee(callee)) return;
|
|
3551
|
-
if (testCalleeHasModifier(callee, "each")) return;
|
|
3552
|
-
const name = testName(node);
|
|
3553
|
-
if (name === void 0) return;
|
|
3554
|
-
if (seen.has(name)) {
|
|
3555
|
-
context.report({
|
|
3556
|
-
data: { name },
|
|
3557
|
-
messageId: "duplicate",
|
|
3558
|
-
node
|
|
3559
|
-
});
|
|
3560
|
-
return;
|
|
3561
|
-
}
|
|
3562
|
-
seen.add(name);
|
|
3563
|
-
} };
|
|
3564
|
-
},
|
|
3565
|
-
meta: {
|
|
3566
|
-
docs: RULE_DOCS["j4-unique-test-names"],
|
|
3567
|
-
messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (J4 — see docs/10-linting.md)." },
|
|
3568
|
-
type: "problem"
|
|
3569
|
-
}
|
|
3570
|
-
};
|
|
3571
|
-
//#endregion
|
|
3572
|
-
//#region src/lint/rules/j5-lowercase-title.ts
|
|
3573
|
-
/** The block/test introducers whose title literal C1' constrains. */
|
|
3574
|
-
const TITLED = /* @__PURE__ */ new Set([
|
|
3575
|
-
"describe",
|
|
3576
|
-
"it",
|
|
3577
|
-
"test"
|
|
3578
|
-
]);
|
|
3579
|
-
/** An identifier-shaped, all-caps/underscored first word: `VALID_CATEGORIES`,
|
|
3580
|
-
* `HTTP`, `DI`. Such a word names a symbol verbatim — lowercasing it would
|
|
3581
|
-
* misspell it — so it is exempt. A prose word (`Rejects`, `Builds`) is not. */
|
|
3582
|
-
const ALL_CAPS_IDENTIFIER = /^[A-Z][A-Z0-9_]*$/u;
|
|
3583
|
-
/**
|
|
3584
|
-
* CONVENTIONS J5 — the first character of a `test('…')` / `describe('…')` title
|
|
3585
|
-
* literal must be lowercase.
|
|
3586
|
-
*
|
|
3587
|
-
* The test name is the sole description of behaviour (B3): a sentence fragment,
|
|
3588
|
-
* lowercase-led like prose (`'rejects an unknown job'`). Exempt are titles
|
|
3589
|
-
* whose first WORD is an identifier-shaped all-caps/underscored token
|
|
3590
|
-
* (`'VALID_CATEGORIES is rejected'`, `'HTTP is upgraded'`, `'DI wires …'`) —
|
|
3591
|
-
* lowercasing a named symbol would misspell it — and titles opening on a
|
|
3592
|
-
* non-letter (a `$`, a `{`, a `%s` placeholder). Only lowercase-able prose
|
|
3593
|
-
* first words are enforced. Covers the modifier and parametrized forms
|
|
3594
|
-
* (`test.only`, `describe.each(...)(...)`) via the chain root.
|
|
3595
|
-
*/
|
|
3596
|
-
const j5LowercaseTitle = {
|
|
3597
|
-
create(context) {
|
|
3598
|
-
return { CallExpression(node) {
|
|
3599
|
-
const callee = node.callee;
|
|
3600
|
-
const root = chainRootName(callee);
|
|
3601
|
-
if (root === void 0 || !TITLED.has(root)) return;
|
|
3602
|
-
const title = stringValue(node.arguments?.[0]);
|
|
3603
|
-
if (title === void 0) return;
|
|
3604
|
-
const first = [...title][0];
|
|
3605
|
-
if (first === void 0 || !/\p{L}/u.test(first)) return;
|
|
3606
|
-
const firstWord = title.split(/\s+/u)[0];
|
|
3607
|
-
if (ALL_CAPS_IDENTIFIER.test(firstWord)) return;
|
|
3608
|
-
if (first !== first.toLocaleLowerCase()) context.report({
|
|
3609
|
-
data: { runner: root },
|
|
3610
|
-
messageId: "uppercase",
|
|
3611
|
-
node
|
|
3612
|
-
});
|
|
3613
|
-
} };
|
|
3614
|
-
},
|
|
3615
|
-
meta: {
|
|
3616
|
-
docs: RULE_DOCS["j5-lowercase-title"],
|
|
3617
|
-
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
|
|
3618
|
-
type: "problem"
|
|
3619
|
-
}
|
|
3620
|
-
};
|
|
3621
|
-
//#endregion
|
|
3622
3435
|
//#region src/lint/rules/w1-scenario-pure.ts
|
|
3623
3436
|
/** The scenario-carrying terminal actions: `.visit()` (website) and `.open()` (mobile). */
|
|
3624
3437
|
const SCENARIO_ACTIONS$1 = /* @__PURE__ */ new Set(["open", "visit"]);
|
|
@@ -3648,7 +3461,7 @@ const w1ScenarioPure = {
|
|
|
3648
3461
|
},
|
|
3649
3462
|
meta: {
|
|
3650
3463
|
docs: RULE_DOCS["w1-scenario-pure"],
|
|
3651
|
-
messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/
|
|
3464
|
+
messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/13-linting.md)." },
|
|
3652
3465
|
type: "problem"
|
|
3653
3466
|
}
|
|
3654
3467
|
};
|
|
@@ -3664,7 +3477,7 @@ const SCENARIO_ACTIONS = /* @__PURE__ */ new Set(["open", "visit"]);
|
|
|
3664
3477
|
*
|
|
3665
3478
|
* Registered in a consumer's (or this repo's own) `oxlint.config.ts` via
|
|
3666
3479
|
* `jsPlugins: ['@jterrazz/test/oxlint']` and referenced as `jterrazz/<rule>` in
|
|
3667
|
-
* the `rules` map, e.g. `'jterrazz/
|
|
3480
|
+
* the `rules` map, e.g. `'jterrazz/j2-no-sleep-in-specs': 'error'` — or enabled
|
|
3668
3481
|
* wholesale by spreading {@link recommendedRules}.
|
|
3669
3482
|
*
|
|
3670
3483
|
* This entry is bundled by tsdown (`dist/oxlint.js`); rules import nothing from
|
|
@@ -3714,11 +3527,7 @@ const plugin = {
|
|
|
3714
3527
|
"i1-layer-boundaries": i1LayerBoundaries,
|
|
3715
3528
|
"i2-sibling-test-naming": i2SiblingTestNaming,
|
|
3716
3529
|
"i4-no-vi-mock-in-src": i4NoViMockInSrc,
|
|
3717
|
-
"j1-no-only-skip": j1NoOnlySkip,
|
|
3718
3530
|
"j2-no-sleep-in-specs": j2NoSleepInSpecs,
|
|
3719
|
-
"j3-no-expectless-test": j3NoExpectlessTest,
|
|
3720
|
-
"j4-unique-test-names": j4UniqueTestNames,
|
|
3721
|
-
"j5-lowercase-title": j5LowercaseTitle,
|
|
3722
3531
|
"w1-scenario-pure": w1ScenarioPure,
|
|
3723
3532
|
"w2w-user-facing-elements": {
|
|
3724
3533
|
create(context) {
|
|
@@ -3740,7 +3549,7 @@ const plugin = {
|
|
|
3740
3549
|
},
|
|
3741
3550
|
meta: {
|
|
3742
3551
|
docs: RULE_DOCS["w2w-user-facing-elements"],
|
|
3743
|
-
messages: { testIdElement: "Prefer a user-facing element (button, link, field, heading, content) over testId() — the escape hatch hides what the user actually sees (W2 — see docs/
|
|
3552
|
+
messages: { testIdElement: "Prefer a user-facing element (button, link, field, heading, content) over testId() — the escape hatch hides what the user actually sees (W2 — see docs/13-linting.md)." },
|
|
3744
3553
|
type: "suggestion"
|
|
3745
3554
|
}
|
|
3746
3555
|
}
|