@systemfsoftware/oxlint-plugin-test-discipline 3.9.0 → 4.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/CHANGELOG.md +6 -0
- package/README.md +18 -16
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +417 -176
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +3 -2
- package/dist/oxlint-plugin-test-discipline.d.ts +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @systemfsoftware/oxlint-plugin-test-discipline
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Contract suites are now a recognised kind of behaviour test. A test file with the `contract` suffix may live beside integration suites, and the rules hold it to the same obligations: exactly one Gherkin feature, no bare test runner, and an import of the package it proves.
|
|
8
|
+
|
|
3
9
|
## 3.9.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,21 +4,22 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
|
-
| Rule | What it enforces
|
|
8
|
-
| ------------------------------------ |
|
|
9
|
-
| `no-test-file-in-src` | Under `src/`, the only sanctioned test file is a single-segment `<stem>.workflow.property.test.ts` inside a sanctioned test directory, plus the generated `schema-laws.test.ts` entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source `import.meta.vitest` block, and a public-surface test moves outside `src/` as an integration test.
|
|
10
|
-
| `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default.
|
|
11
|
-
| `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`.
|
|
12
|
-
| `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, or `.
|
|
13
|
-
| `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`.
|
|
14
|
-
| `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule.
|
|
15
|
-
| `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest` or `@
|
|
16
|
-
| `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case.
|
|
17
|
-
| `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal.
|
|
18
|
-
| `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant.
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `vitest-from-
|
|
7
|
+
| Rule | What it enforces |
|
|
8
|
+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `no-test-file-in-src` | Under `src/`, the only sanctioned test file is a single-segment `<stem>.workflow.property.test.ts` inside a sanctioned test directory, plus the generated `schema-laws.test.ts` entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source `import.meta.vitest` block, and a public-surface test moves outside `src/` as an integration test. |
|
|
10
|
+
| `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default. |
|
|
11
|
+
| `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`. |
|
|
12
|
+
| `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, `.trace.test.ts`, or `.conformance.test.ts` — the four sanctioned lane suffixes. A runner-driving package (`vitest`, `vitest-conformance`, `effect-spec-runtime`) is exempt: its plain `*.test.ts` runners and nested-run probes are not consumer lanes. |
|
|
13
|
+
| `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`. |
|
|
14
|
+
| `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule. |
|
|
15
|
+
| `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest`, `@effect/vitest` or `@systemfsoftware/vitest`. |
|
|
16
|
+
| `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case. |
|
|
17
|
+
| `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal. |
|
|
18
|
+
| `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest`/`@systemfsoftware/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant. The harness is expressed as `Differential.compare({ name, reference, candidate })` or `Metamorphic.on({ name, system })`. |
|
|
19
|
+
| `conformance-test-requires-harness` | A `.conformance.test.ts` must import `Conformance` from `@systemfsoftware/conformance-spec` and invoke one of its checks — `Conformance.linearizable`, `Conformance.sequential`, or `Conformance.released`; direct `vitest`/`@effect/vitest`/`@systemfsoftware/vitest` imports, raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases), and the retired `Linearizable.check` / `SequentialModel.check` / `Released.check` shapes are forbidden, and importing the barrel without invoking a check is equally non-compliant. |
|
|
20
|
+
| `model-fixture-imports-subject` | A `*.model.ts` fixture may not import the package whose tests it serves. The package is read from the fixture's path — the directory its tests tree lives in — and an import, re-export, dynamic import, or require of that name, or of any subpath of it, is the implementation reaching its own oracle. A relative specifier is judged the same way lexically: resolved against the fixture's directory it must stay inside the tests tree, so `../../src/Lock.js` is reported and `./Locks.js` is a sibling fixture. `effect`, node builtins, and other packages' names stay allowed. |
|
|
21
|
+
| `vitest-from-systemfsoftware-vitest` | Every value a test uses from Vitest is imported from `@systemfsoftware/vitest`, which re-exports all of Vitest. Any value import from `vitest` or the upstream `@effect/vitest` — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal. Exempt: the packages that drive Vitest's own runner instead of the fork (`vitest` itself, `vitest-conformance`, and `effect-spec-runtime`, whose `Register` calls Vitest's option-taking collector). |
|
|
22
|
+
| `tests-import-public-api` | Every file under the package-root `tests/` or `__tests__/` trees, regardless of basename, may not relative-import `src` or climb into an `internal` folder. Import the published package name or a sibling helper. |
|
|
22
23
|
|
|
23
24
|
| suffix | layer | doubles | location |
|
|
24
25
|
| ----------------------------- | ------------ | ------------------------ | ------------------ |
|
|
@@ -26,10 +27,11 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
26
27
|
| `*.integration.test.ts` | Behaviour | permitted, at ports only | NEVER under `src/` |
|
|
27
28
|
| `*.differential.test.ts` | Differential | permitted, at ports only | NEVER under `src/` |
|
|
28
29
|
| `*.trace.test.ts` | Trace spec | none — observes graphs | NEVER under `src/` |
|
|
30
|
+
| `*.conformance.test.ts` | Conformance | permitted, at ports only | NEVER under `src/` |
|
|
29
31
|
|
|
30
32
|
## Enrollment
|
|
31
33
|
|
|
32
|
-
`@systemfsoftware/oxlint-config-recommended` loads this plugin in `jsPlugins` and
|
|
34
|
+
`@systemfsoftware/oxlint-config-recommended` loads this plugin in `jsPlugins` and spreads `configs.recommended` into its default `rules` at `error`, so every rule above fires in every package that extends the preset.
|
|
33
35
|
|
|
34
36
|
## Testing
|
|
35
37
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,11 +25,12 @@ declare const _default: {
|
|
|
25
25
|
'behaviour-one-feature-per-file': import("@oxlint/plugins").Rule;
|
|
26
26
|
'no-pseudo-gherkin-unit-tests': import("@oxlint/plugins").Rule;
|
|
27
27
|
'tests-dir-helpers-in-fixtures': import("@oxlint/plugins").Rule;
|
|
28
|
-
'
|
|
29
|
-
'vitest-from-effect-vitest': import("@oxlint/plugins").Rule;
|
|
28
|
+
'vitest-from-systemfsoftware-vitest': import("@oxlint/plugins").Rule;
|
|
30
29
|
'no-io-module-in-source-test': import("@oxlint/plugins").Rule;
|
|
31
30
|
'tests-import-public-api': import("@oxlint/plugins").Rule;
|
|
32
31
|
'differential-test-requires-harness': import("@oxlint/plugins").Rule;
|
|
32
|
+
'conformance-test-requires-harness': import("@oxlint/plugins").Rule;
|
|
33
|
+
'model-fixture-imports-subject': import("@oxlint/plugins").Rule;
|
|
33
34
|
'ban-raw-span-name-emit': import("@oxlint/plugins").Rule;
|
|
34
35
|
'trace-test-requires-taxonomy': import("@oxlint/plugins").Rule;
|
|
35
36
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const EMIT_CALLEES = [
|
|
|
8
8
|
];
|
|
9
9
|
const EXPECTED$3 = "a span declared once with Span.declare({ id, name, attrs }) and started through the start(attrs) it returns";
|
|
10
10
|
const FIX$3 = "declare the span with Span.declare({ id, name, attrs }) and emit it with SomeSpan.start(attrs)";
|
|
11
|
-
const meta$
|
|
11
|
+
const meta$28 = {
|
|
12
12
|
type: "problem",
|
|
13
13
|
docs: { description: "Ban a raw span name passed to an emit API (startSpan, spanBuilder, startActiveSpan). Span names enter through Span.declare, never as a literal at the emit site." },
|
|
14
14
|
schema: [],
|
|
@@ -18,14 +18,14 @@ const meta$27 = {
|
|
|
18
18
|
//#region src/rules/ban-raw-span-name-emit.ts
|
|
19
19
|
const INTERPOLATION = "${}";
|
|
20
20
|
const isEmitCallee$1 = (name) => EMIT_CALLEES.some((emit) => emit === name);
|
|
21
|
-
const memberPropertyName = (callee) => {
|
|
21
|
+
const memberPropertyName$1 = (callee) => {
|
|
22
22
|
if (callee.computed) return null;
|
|
23
23
|
if (callee.property.type !== "Identifier") return null;
|
|
24
24
|
return callee.property.name;
|
|
25
25
|
};
|
|
26
|
-
const calleeName$
|
|
26
|
+
const calleeName$2 = (callee) => {
|
|
27
27
|
if (callee.type === "Identifier") return callee.name;
|
|
28
|
-
if (callee.type === "MemberExpression") return memberPropertyName(callee);
|
|
28
|
+
if (callee.type === "MemberExpression") return memberPropertyName$1(callee);
|
|
29
29
|
return null;
|
|
30
30
|
};
|
|
31
31
|
const templateName = (node) => node.quasis.map((quasi) => quasi.value.raw).join(INTERPOLATION);
|
|
@@ -39,7 +39,7 @@ const argName = (arg) => {
|
|
|
39
39
|
return inlineName(arg);
|
|
40
40
|
};
|
|
41
41
|
const inspect = (node) => {
|
|
42
|
-
const callee = calleeName$
|
|
42
|
+
const callee = calleeName$2(node.callee);
|
|
43
43
|
if (callee === null || !isEmitCallee$1(callee)) return null;
|
|
44
44
|
const name = argName(node.arguments[0]);
|
|
45
45
|
if (name === null) return null;
|
|
@@ -49,7 +49,7 @@ const inspect = (node) => {
|
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
const banRawSpanNameEmit = defineRule({
|
|
52
|
-
meta: meta$
|
|
52
|
+
meta: meta$28,
|
|
53
53
|
create(context) {
|
|
54
54
|
return { CallExpression(node) {
|
|
55
55
|
const emitCall = inspect(node);
|
|
@@ -71,6 +71,7 @@ const banRawSpanNameEmit = defineRule({
|
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/rules/path.config.ts
|
|
73
73
|
const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
|
|
74
|
+
const TYPE_TEST_BASENAME = /\.tst\.[cm]?tsx?$/;
|
|
74
75
|
const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
|
|
75
76
|
/**
|
|
76
77
|
* The only test location sanctioned under `src/`. A workflow property test
|
|
@@ -80,7 +81,6 @@ const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
|
|
|
80
81
|
const NESTED_TEST_DIR = "__tests__";
|
|
81
82
|
const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
|
|
82
83
|
const PROPERTY_SUFFIX = ".property.test.ts";
|
|
83
|
-
const INTEGRATION_SUFFIX = ".integration.test.ts";
|
|
84
84
|
const DIFFERENTIAL_SUFFIX = ".differential.test.ts";
|
|
85
85
|
/**
|
|
86
86
|
* Forbidden outright. A schema's laws are generated, so a hand-written
|
|
@@ -97,8 +97,19 @@ const SCHEMA_SUFFIX = ".schema.test.ts";
|
|
|
97
97
|
const WORKFLOW_TEST_BASENAME = /^[^.]+\.workflow\.property\.test\.ts$/;
|
|
98
98
|
const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
|
|
99
99
|
const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
|
|
100
|
+
const CONFORMANCE_PACKAGE = "@systemfsoftware/conformance-spec";
|
|
100
101
|
const TRACE_SPEC_PACKAGE = "@systemfsoftware/trace-spec";
|
|
101
|
-
const
|
|
102
|
+
const LAWFUL_RUNNER = "@systemfsoftware/vitest";
|
|
103
|
+
const FOREIGN_VITEST_SOURCES = {
|
|
104
|
+
"vitest": true,
|
|
105
|
+
"@effect/vitest": true
|
|
106
|
+
};
|
|
107
|
+
/** Runners a harness-owned test (Gherkin, conformance, differential) must not import directly. */
|
|
108
|
+
const FOREIGN_RUNNERS = {
|
|
109
|
+
...FOREIGN_VITEST_SOURCES,
|
|
110
|
+
[LAWFUL_RUNNER]: true
|
|
111
|
+
};
|
|
112
|
+
/** Packages whose own tests drive Vitest directly: the runner and its conformance harness. */
|
|
102
113
|
const RAW_VITEST_PACKAGES = {
|
|
103
114
|
"effect-spec-runtime": true,
|
|
104
115
|
"vitest": true,
|
|
@@ -109,19 +120,19 @@ const RUNNER_NAMES = /* @__PURE__ */ new Set([
|
|
|
109
120
|
"test",
|
|
110
121
|
"describe"
|
|
111
122
|
]);
|
|
112
|
-
const MESSAGE$
|
|
123
|
+
const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
113
124
|
const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
114
125
|
//#endregion
|
|
115
126
|
//#region src/rules/behaviour-exercises-use-case.config.ts
|
|
116
127
|
const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package code";
|
|
117
128
|
const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
|
|
118
|
-
const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
|
|
129
|
+
const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
|
|
119
130
|
const NO_SUBJECT_IMPORT_FIX = "a behaviour test exercises a use case, so it has to reach the package. A file that imports nothing but its runner and effect is asserting over values it built in the same file. Ask whether the assertion tests anything at all: if it restates a literal, delete the scenario; if it states an invariant that holds over generated inputs, move it to a *.property.test.ts beside the cell that decides it. Type-only imports never count - they are erased before anything runs - while a side-effect import (import \"../src/x.js\", import {} from \"../src/x.js\") does count, because it executes that module.";
|
|
120
|
-
const meta$
|
|
131
|
+
const meta$27 = {
|
|
121
132
|
type: "problem",
|
|
122
133
|
docs: { description: "A *.integration.test.ts must import the package under test, not only its runner and effect, so the scenario exercises code that ships rather than values the test built itself." },
|
|
123
134
|
schema: [],
|
|
124
|
-
messages: { noSubjectImport: MESSAGE$
|
|
135
|
+
messages: { noSubjectImport: MESSAGE$8 }
|
|
125
136
|
};
|
|
126
137
|
//#endregion
|
|
127
138
|
//#region src/rules/path.ts
|
|
@@ -141,13 +152,14 @@ const isInTestsImportScope = (filename) => {
|
|
|
141
152
|
};
|
|
142
153
|
const isInConfiguredTestDir = (filename, dirs) => directoriesOf(filename).some((segment) => dirs.includes(segment));
|
|
143
154
|
const isTestFile = (basename) => TEST_BASENAME.test(basename);
|
|
155
|
+
const isBehaviourBasename = (basename) => basename.endsWith(".integration.test.ts") || basename.endsWith(".contract.test.ts");
|
|
144
156
|
const CELL_SUFFIX = /\.([^.]+)\.[cm]?tsx?$/;
|
|
145
157
|
const cellOf = (basename) => CELL_SUFFIX.exec(basename)?.[1];
|
|
146
158
|
//#endregion
|
|
147
159
|
//#region src/rules/behaviour-exercises-use-case.ts
|
|
148
160
|
const EFFECT_PACKAGE = "effect";
|
|
149
161
|
const FOUNDATION_PACKAGES = /* @__PURE__ */ new Set([
|
|
150
|
-
...FOREIGN_RUNNERS,
|
|
162
|
+
...Object.keys(FOREIGN_RUNNERS),
|
|
151
163
|
GHERKIN_PACKAGE,
|
|
152
164
|
EFFECT_PACKAGE
|
|
153
165
|
]);
|
|
@@ -170,7 +182,6 @@ const isFoundationImport = (source, filename) => {
|
|
|
170
182
|
if (source === "@systemfsoftware/effect-gherkin-spec" && isGherkinPackageTree(filename)) return false;
|
|
171
183
|
return FOUNDATION_PACKAGES.has(source) || source.startsWith(`${EFFECT_PACKAGE}/`) || source.startsWith("node:");
|
|
172
184
|
};
|
|
173
|
-
const isBehaviourTest$3 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
174
185
|
/** The stem of a path with its final extension stripped, for identity comparisons. */
|
|
175
186
|
const stemOf = (file) => file.replace(/\.[^/]+$/, "");
|
|
176
187
|
/**
|
|
@@ -222,16 +233,17 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
|
|
|
222
233
|
* name documents it for the reader without pretending to be evidence.
|
|
223
234
|
*
|
|
224
235
|
* What counts as reaching: a runtime import from anything that is not the
|
|
225
|
-
* scaffolding - vitest, @effect/vitest, the gherkin
|
|
226
|
-
* subpath of it, or a Node builtin - and not the test
|
|
227
|
-
* `import(...)` of such a source. Type-only
|
|
228
|
-
* count; a side-effect import (`import
|
|
229
|
-
* executes its module and counts for
|
|
230
|
-
*
|
|
231
|
-
*
|
|
236
|
+
* scaffolding - vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin
|
|
237
|
+
* spec package, effect or a subpath of it, or a Node builtin - and not the test
|
|
238
|
+
* file itself, or a dynamic `import(...)` of such a source. Type-only
|
|
239
|
+
* specifiers are erased and never count; a side-effect import (`import
|
|
240
|
+
* "./x.js"`, `import {} from "./x.js"`) executes its module and counts for
|
|
241
|
+
* whatever that module is. Whether a named module actually exists is not
|
|
242
|
+
* observable from one file, so an import whose path names nothing satisfies the
|
|
243
|
+
* rule the same way a real one does.
|
|
232
244
|
*/
|
|
233
245
|
const behaviourExercisesUseCase = defineRule({
|
|
234
|
-
meta: meta$
|
|
246
|
+
meta: meta$27,
|
|
235
247
|
create(context) {
|
|
236
248
|
let reached = false;
|
|
237
249
|
return {
|
|
@@ -246,7 +258,7 @@ const behaviourExercisesUseCase = defineRule({
|
|
|
246
258
|
if (typeof node.value === "string" && DIST_SEGMENT.test(node.value)) reached = true;
|
|
247
259
|
},
|
|
248
260
|
"Program:exit"(node) {
|
|
249
|
-
if (!
|
|
261
|
+
if (!isBehaviourBasename(basenameOf(context.filename))) return;
|
|
250
262
|
if (reached) return;
|
|
251
263
|
for (const statement of node.body) {
|
|
252
264
|
if (statement.type !== "ImportDeclaration") continue;
|
|
@@ -281,13 +293,13 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
|
|
|
281
293
|
const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
|
|
282
294
|
const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
|
|
283
295
|
const TOO_MANY_FEATURES_FIX = "splitting a junk drawer into several smaller junk drawers is not an improvement. When separating scenarios surfaces assertions that restate a pure function return value — change detectors against a lookup table or constant — those get deleted, not rehoused. Each surviving capability keeps its own file with exactly one Feature.";
|
|
284
|
-
const meta$
|
|
296
|
+
const meta$26 = {
|
|
285
297
|
type: "problem",
|
|
286
298
|
docs: { description: "A *.integration.test.ts must contain exactly one Feature(...) call. Zero or two-or-more is the junk-drawer signal that produced 41 scenarios of pure-function assertions in a single file." },
|
|
287
299
|
schema: [],
|
|
288
300
|
messages: {
|
|
289
|
-
tooFewFeatures: MESSAGE$
|
|
290
|
-
tooManyFeatures: MESSAGE$
|
|
301
|
+
tooFewFeatures: MESSAGE$8,
|
|
302
|
+
tooManyFeatures: MESSAGE$8
|
|
291
303
|
}
|
|
292
304
|
};
|
|
293
305
|
//#endregion
|
|
@@ -306,7 +318,6 @@ const isFeatureCallStatement = (statement) => {
|
|
|
306
318
|
if (statement.expression.type !== "CallExpression") return false;
|
|
307
319
|
return rootCalleeName(statement.expression.callee) === "Feature";
|
|
308
320
|
};
|
|
309
|
-
const isBehaviourTest$2 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
310
321
|
const findSecondFeatureCall = (program) => {
|
|
311
322
|
let seen = 0;
|
|
312
323
|
for (const statement of program.body) {
|
|
@@ -326,10 +337,10 @@ const hasAnyFeatureCall = (program) => {
|
|
|
326
337
|
return false;
|
|
327
338
|
};
|
|
328
339
|
const behaviourOneFeaturePerFile = defineRule({
|
|
329
|
-
meta: meta$
|
|
340
|
+
meta: meta$26,
|
|
330
341
|
create(context) {
|
|
331
342
|
return { "Program:exit"(node) {
|
|
332
|
-
if (!
|
|
343
|
+
if (!isBehaviourBasename(basenameOf(context.filename))) return;
|
|
333
344
|
const excess = findSecondFeatureCall(node);
|
|
334
345
|
if (excess !== null) {
|
|
335
346
|
const total = countFeatureCalls(node);
|
|
@@ -361,19 +372,19 @@ const behaviourOneFeaturePerFile = defineRule({
|
|
|
361
372
|
//#endregion
|
|
362
373
|
//#region src/rules/behaviour-test-requires-gherkin.config.ts
|
|
363
374
|
const FOREIGN_RUNNER_EXPECTED = "it and layer imported from @systemfsoftware/effect-gherkin-spec";
|
|
364
|
-
const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest or @
|
|
365
|
-
const FOREIGN_RUNNER_FIX = "import { it
|
|
375
|
+
const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest, @effect/vitest or @systemfsoftware/vitest in a behaviour file";
|
|
376
|
+
const FOREIGN_RUNNER_FIX = "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it })";
|
|
366
377
|
const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
|
|
367
378
|
const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
|
|
368
379
|
const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
|
|
369
380
|
const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
|
|
370
|
-
const meta$
|
|
381
|
+
const meta$25 = {
|
|
371
382
|
type: "problem",
|
|
372
|
-
docs: { description: "A *.integration.test.ts must drive its suite through makeFeature from @systemfsoftware/effect-gherkin-spec and must not import test runners from vitest or @
|
|
383
|
+
docs: { description: "A *.integration.test.ts must drive its suite through makeFeature from @systemfsoftware/effect-gherkin-spec and must not import test runners from vitest, @effect/vitest or @systemfsoftware/vitest." },
|
|
373
384
|
schema: [],
|
|
374
385
|
messages: {
|
|
375
|
-
foreignRunner: MESSAGE$
|
|
376
|
-
missingMakeFeature: MESSAGE$
|
|
386
|
+
foreignRunner: MESSAGE$8,
|
|
387
|
+
missingMakeFeature: MESSAGE$8
|
|
377
388
|
}
|
|
378
389
|
};
|
|
379
390
|
//#endregion
|
|
@@ -393,13 +404,12 @@ const foreignRunnerNameOf = (specifier) => {
|
|
|
393
404
|
const { name } = Schema.decodeSync(ImportedIdentifier)(specifier.imported);
|
|
394
405
|
return RUNNER_NAMES.has(name) ? name : null;
|
|
395
406
|
};
|
|
396
|
-
const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
397
407
|
const behaviourTestRequiresGherkin = defineRule({
|
|
398
|
-
meta: meta$
|
|
408
|
+
meta: meta$25,
|
|
399
409
|
create(context) {
|
|
400
410
|
const basename = basenameOf(context.filename);
|
|
401
411
|
return { Program(node) {
|
|
402
|
-
if (!
|
|
412
|
+
if (!isBehaviourBasename(basename)) return;
|
|
403
413
|
let hasMakeFeature = false;
|
|
404
414
|
for (const statement of node.body) {
|
|
405
415
|
if (statement.type !== "ImportDeclaration") continue;
|
|
@@ -408,7 +418,7 @@ const behaviourTestRequiresGherkin = defineRule({
|
|
|
408
418
|
if (specifier.type !== "ImportSpecifier") continue;
|
|
409
419
|
if (isMakeFeatureSpecifier(specifier)) hasMakeFeature = true;
|
|
410
420
|
}
|
|
411
|
-
if (
|
|
421
|
+
if (FOREIGN_RUNNERS[sourceValue] !== true) continue;
|
|
412
422
|
for (const specifier of statement.specifiers) {
|
|
413
423
|
if (specifier.type !== "ImportSpecifier") continue;
|
|
414
424
|
const runnerName = foreignRunnerNameOf(specifier);
|
|
@@ -438,6 +448,145 @@ const behaviourTestRequiresGherkin = defineRule({
|
|
|
438
448
|
} };
|
|
439
449
|
}
|
|
440
450
|
});
|
|
451
|
+
const HARNESS_MEMBERS = {
|
|
452
|
+
linearizable: true,
|
|
453
|
+
sequential: true,
|
|
454
|
+
released: true
|
|
455
|
+
};
|
|
456
|
+
const LEGACY_CHECK_MEMBER = "check";
|
|
457
|
+
const HARNESS_PRESCRIPTION$2 = "import { Conformance } from @systemfsoftware/conformance-spec and express the test as Conformance.linearizable(implementation, { commands, model, run, fibers, operations }) in the scenario body";
|
|
458
|
+
const meta$24 = {
|
|
459
|
+
type: "problem",
|
|
460
|
+
docs: { description: "Conformance tests must use the @systemfsoftware/conformance-spec Conformance barrel: Conformance.linearizable, Conformance.sequential, or Conformance.released. Raw runner calls (it, test, describe, and member forms like it.effect) and direct runner imports are forbidden in *.conformance.test.ts files; the retired Linearizable.check / SequentialModel.check / Released.check shapes are rejected, and importing Conformance without invoking a check is equally non-compliant." },
|
|
461
|
+
schema: [],
|
|
462
|
+
messages: {
|
|
463
|
+
rawRunnerCall: MESSAGE$8,
|
|
464
|
+
runnerImport: MESSAGE$8,
|
|
465
|
+
missingHarnessImport: MESSAGE$8,
|
|
466
|
+
legacyHarnessCall: MESSAGE$8,
|
|
467
|
+
missingHarnessUsage: MESSAGE$8
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/rules/conformance-test-requires-harness.ts
|
|
472
|
+
const memberObjectName$1 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
473
|
+
const memberPropertyName = (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" ? callee.property.name : void 0;
|
|
474
|
+
const memberName = (callee) => callee.type === "Identifier" ? callee.name : memberPropertyName(callee);
|
|
475
|
+
const calleeName$1 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$1(callee);
|
|
476
|
+
const isRawRunnerCall = (callee) => {
|
|
477
|
+
const name = calleeName$1(callee);
|
|
478
|
+
return name !== void 0 && RUNNER_NAMES.has(name);
|
|
479
|
+
};
|
|
480
|
+
const isForeignRunnerImport$1 = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
|
|
481
|
+
const namesConformanceExport = (specifier) => specifier.imported.type === "Identifier" && specifier.imported.name === "Conformance";
|
|
482
|
+
const recordBindings = (node, packageBindings, conformanceBindings) => {
|
|
483
|
+
if (node.source.value !== "@systemfsoftware/conformance-spec") return;
|
|
484
|
+
for (const specifier of node.specifiers) {
|
|
485
|
+
packageBindings.add(specifier.local.name);
|
|
486
|
+
if (specifier.type === "ImportSpecifier" && namesConformanceExport(specifier)) conformanceBindings.add(specifier.local.name);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const isHarnessInvocation = (callee, conformanceBindings) => {
|
|
490
|
+
const member = memberName(callee);
|
|
491
|
+
const root = calleeName$1(callee);
|
|
492
|
+
return member !== void 0 && HARNESS_MEMBERS[member] === true && root !== void 0 && conformanceBindings.has(root);
|
|
493
|
+
};
|
|
494
|
+
const isLegacyCheck = (callee, packageBindings) => {
|
|
495
|
+
const root = calleeName$1(callee);
|
|
496
|
+
return memberName(callee) === "check" && root !== void 0 && packageBindings.has(root);
|
|
497
|
+
};
|
|
498
|
+
const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$1(callee)}.${LEGACY_CHECK_MEMBER}`;
|
|
499
|
+
const legacyCheckError = (callee) => ({
|
|
500
|
+
messageId: "legacyHarnessCall",
|
|
501
|
+
data: {
|
|
502
|
+
name: `a .${LEGACY_CHECK_MEMBER}(...) call in a conformance test file`,
|
|
503
|
+
expected: HARNESS_PRESCRIPTION$2,
|
|
504
|
+
actual: `${legacyCallText(callee)} is not a check the barrel exposes`,
|
|
505
|
+
fix: HARNESS_PRESCRIPTION$2
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
const conformanceTestRequiresHarness = defineRule({
|
|
509
|
+
meta: meta$24,
|
|
510
|
+
create(context) {
|
|
511
|
+
if (!context.filename.endsWith(".conformance.test.ts")) return {};
|
|
512
|
+
const packageBindings = /* @__PURE__ */ new Set();
|
|
513
|
+
const conformanceBindings = /* @__PURE__ */ new Set();
|
|
514
|
+
let violations = 0;
|
|
515
|
+
let hasHarnessInvocation = false;
|
|
516
|
+
return {
|
|
517
|
+
ImportDeclaration(node) {
|
|
518
|
+
if (isForeignRunnerImport$1(node)) {
|
|
519
|
+
context.report({
|
|
520
|
+
node,
|
|
521
|
+
messageId: "runnerImport",
|
|
522
|
+
data: {
|
|
523
|
+
name: `runner import from ${String(node.source.value)} in a conformance test file`,
|
|
524
|
+
expected: HARNESS_PRESCRIPTION$2,
|
|
525
|
+
actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the conformance check",
|
|
526
|
+
fix: `delete the runner import; ${HARNESS_PRESCRIPTION$2}`
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
violations += 1;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
recordBindings(node, packageBindings, conformanceBindings);
|
|
533
|
+
},
|
|
534
|
+
CallExpression(node) {
|
|
535
|
+
if (isRawRunnerCall(node.callee)) {
|
|
536
|
+
const name = calleeName$1(node.callee) ?? "";
|
|
537
|
+
context.report({
|
|
538
|
+
node,
|
|
539
|
+
messageId: "rawRunnerCall",
|
|
540
|
+
data: {
|
|
541
|
+
name: `raw runner call (${name}) in a conformance test file`,
|
|
542
|
+
expected: HARNESS_PRESCRIPTION$2,
|
|
543
|
+
actual: `${name}(...) bypasses the conformance check`,
|
|
544
|
+
fix: `rewrite using ${HARNESS_PRESCRIPTION$2}`
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
violations += 1;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (isHarnessInvocation(node.callee, conformanceBindings)) {
|
|
551
|
+
hasHarnessInvocation = true;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (isLegacyCheck(node.callee, packageBindings)) {
|
|
555
|
+
context.report({
|
|
556
|
+
node,
|
|
557
|
+
...legacyCheckError(node.callee)
|
|
558
|
+
});
|
|
559
|
+
violations += 1;
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
"Program:exit"(node) {
|
|
563
|
+
if (packageBindings.size === 0) {
|
|
564
|
+
context.report({
|
|
565
|
+
node,
|
|
566
|
+
messageId: "missingHarnessImport",
|
|
567
|
+
data: {
|
|
568
|
+
name: `conformance test file without the ${CONFORMANCE_PACKAGE} import`,
|
|
569
|
+
expected: HARNESS_PRESCRIPTION$2,
|
|
570
|
+
actual: "no conformance check import found",
|
|
571
|
+
fix: HARNESS_PRESCRIPTION$2
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (!hasHarnessInvocation && violations === 0) context.report({
|
|
577
|
+
node,
|
|
578
|
+
messageId: "missingHarnessUsage",
|
|
579
|
+
data: {
|
|
580
|
+
name: `conformance test file imports ${CONFORMANCE_PACKAGE} but never invokes it`,
|
|
581
|
+
expected: HARNESS_PRESCRIPTION$2,
|
|
582
|
+
actual: "no Conformance.linearizable, Conformance.sequential, or Conformance.released call runs",
|
|
583
|
+
fix: HARNESS_PRESCRIPTION$2
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
});
|
|
441
590
|
//#endregion
|
|
442
591
|
//#region src/rules/damp-test-naming.config.ts
|
|
443
592
|
const PASCAL_CASE$1 = /^[A-Z][a-z][a-zA-Z0-9]*$/;
|
|
@@ -540,6 +689,7 @@ const isTestFunctionCall = (node) => {
|
|
|
540
689
|
const dampTestNaming = defineRule({
|
|
541
690
|
meta: meta$23,
|
|
542
691
|
create(context) {
|
|
692
|
+
if (TYPE_TEST_BASENAME.test(basenameOf(context.filename))) return {};
|
|
543
693
|
return { CallExpression(node) {
|
|
544
694
|
if (!isTestFunctionCall(node)) return;
|
|
545
695
|
const firstArg = node.arguments[0];
|
|
@@ -574,14 +724,14 @@ const meta$22 = {
|
|
|
574
724
|
};
|
|
575
725
|
//#endregion
|
|
576
726
|
//#region src/rules/differential-test-requires-harness.ts
|
|
577
|
-
const HARNESS_PRESCRIPTION$1 = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ reference, candidate }).on(arb).assert(oracle) or Metamorphic.on(system).relation({ transformInput, assertOutput }).on(arb)";
|
|
727
|
+
const HARNESS_PRESCRIPTION$1 = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ name, reference, candidate }).on(arb).assert(oracle) or Metamorphic.on({ name, system }).relation({ transformInput, assertOutput }).on(arb)";
|
|
578
728
|
const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
579
729
|
const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
|
|
580
730
|
const recordHarnessBindings = (node, bindings) => {
|
|
581
731
|
if (node.source.value !== "@systemfsoftware/differential-spec") return;
|
|
582
732
|
for (const specifier of node.specifiers) bindings.add(specifier.local.name);
|
|
583
733
|
};
|
|
584
|
-
const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS
|
|
734
|
+
const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
|
|
585
735
|
const differentialTestRequiresHarness = defineRule({
|
|
586
736
|
meta: meta$22,
|
|
587
737
|
create(context) {
|
|
@@ -598,7 +748,7 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
598
748
|
data: {
|
|
599
749
|
name: `runner import from ${String(node.source.value)} in a differential test file`,
|
|
600
750
|
expected: HARNESS_PRESCRIPTION$1,
|
|
601
|
-
actual: "a direct vitest / @effect/vitest runner import bypasses the differential oracle",
|
|
751
|
+
actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the differential oracle",
|
|
602
752
|
fix: `delete the runner import; ${HARNESS_PRESCRIPTION$1}`
|
|
603
753
|
}
|
|
604
754
|
});
|
|
@@ -654,97 +804,16 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
654
804
|
}
|
|
655
805
|
});
|
|
656
806
|
//#endregion
|
|
657
|
-
//#region src/rules/expect-boolean-predicate.config.ts
|
|
658
|
-
const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
659
|
-
const BOOLEAN_MATCHERS = {
|
|
660
|
-
toBe: true,
|
|
661
|
-
toEqual: true
|
|
662
|
-
};
|
|
663
|
-
const COMPARISON_OPERATORS = {
|
|
664
|
-
"===": true,
|
|
665
|
-
"!==": true,
|
|
666
|
-
"==": true,
|
|
667
|
-
"!=": true,
|
|
668
|
-
"<": true,
|
|
669
|
-
"<=": true,
|
|
670
|
-
">": true,
|
|
671
|
-
">=": true,
|
|
672
|
-
instanceof: true,
|
|
673
|
-
in: true
|
|
674
|
-
};
|
|
675
|
-
const VIOLATION_NAME$4 = "a predicate evaluated in the test asserted against true or false";
|
|
676
|
-
const VIOLATION_EXPECTED$1 = "expect(...) around a boolean the code under test returned";
|
|
677
|
-
const VIOLATION_ACTUAL$1 = "expect(...) wraps a call, a comparison, a `!` or an `&&` / `||` evaluated in the test, so a failure reports `expected false to be true` instead of the values that disagreed";
|
|
678
|
-
const VIOLATION_FIX$1 = "rewrite a predicate call as expect(x).toSatisfy(P) — e.g. expect(s.reading).toSatisfy(Result.isFailure) — and a comparison as expect(a).toEqual(b)";
|
|
679
|
-
const meta$21 = {
|
|
680
|
-
type: "problem",
|
|
681
|
-
docs: { description: "An expect(...) asserted with toBe(true|false) or toEqual(true|false) must wrap a boolean the code under test returned. A predicate call, comparison, negation or && / || evaluated in the test is refused, with the toSatisfy or toEqual rewrite as the fix." },
|
|
682
|
-
schema: [],
|
|
683
|
-
messages: { booleanPredicate: MESSAGE$8 }
|
|
684
|
-
};
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/rules/expect-call.ts
|
|
687
|
-
const EXPECT = "expect";
|
|
688
|
-
const isExpectCallee = (callee) => callee.type === "Identifier" && callee.name === EXPECT;
|
|
689
|
-
const expectCallOf = (node, acceptsMember) => {
|
|
690
|
-
if (node.callee.type !== "MemberExpression") return void 0;
|
|
691
|
-
let target = node.callee.object;
|
|
692
|
-
while (target.type === "MemberExpression") {
|
|
693
|
-
if (acceptsMember !== void 0 && acceptsMember(target) === false) return void 0;
|
|
694
|
-
target = target.object;
|
|
695
|
-
}
|
|
696
|
-
if (target.type !== "CallExpression" || isExpectCallee(target.callee) === false) return void 0;
|
|
697
|
-
return target;
|
|
698
|
-
};
|
|
699
|
-
//#endregion
|
|
700
|
-
//#region src/rules/expect-boolean-predicate.ts
|
|
701
|
-
const isNotMember = (member) => member.property.type === "Identifier" && member.property.name === "not";
|
|
702
|
-
const isBooleanLiteral = (node) => node !== void 0 && node.type === "Literal" && typeof node.value === "boolean";
|
|
703
|
-
/** The ARG positions `expect` exposes that no subject can fill by accident. */
|
|
704
|
-
const isEvaluatedInTest = (arg) => {
|
|
705
|
-
switch (arg.type) {
|
|
706
|
-
case "CallExpression": return true;
|
|
707
|
-
case "UnaryExpression": return arg.operator === "!";
|
|
708
|
-
case "LogicalExpression": return arg.operator === "&&" || arg.operator === "||";
|
|
709
|
-
case "BinaryExpression": return COMPARISON_OPERATORS[arg.operator] === true;
|
|
710
|
-
default: return false;
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
const expectBooleanPredicate = defineRule({
|
|
714
|
-
meta: meta$21,
|
|
715
|
-
create(context) {
|
|
716
|
-
return { CallExpression(node) {
|
|
717
|
-
if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier") return;
|
|
718
|
-
if (BOOLEAN_MATCHERS[node.callee.property.name] !== true) return;
|
|
719
|
-
if (!isBooleanLiteral(node.arguments[0])) return;
|
|
720
|
-
const receiver = expectCallOf(node, isNotMember);
|
|
721
|
-
if (receiver === void 0) return;
|
|
722
|
-
const arg = receiver.arguments[0];
|
|
723
|
-
if (arg === void 0 || !isEvaluatedInTest(arg)) return;
|
|
724
|
-
context.report({
|
|
725
|
-
node,
|
|
726
|
-
messageId: "booleanPredicate",
|
|
727
|
-
data: {
|
|
728
|
-
name: VIOLATION_NAME$4,
|
|
729
|
-
expected: VIOLATION_EXPECTED$1,
|
|
730
|
-
actual: VIOLATION_ACTUAL$1,
|
|
731
|
-
fix: VIOLATION_FIX$1
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
} };
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
//#endregion
|
|
738
807
|
//#region src/rules/in-source-test-prop-only.config.ts
|
|
739
808
|
const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
|
|
740
809
|
const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
|
|
741
810
|
const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
|
|
742
811
|
const NON_PROP_CALL_FIX = "delete the block — non-property in-source tests belong nowhere in `src/`; re-home a meaningful example through the cell public export as `*.integration.test.ts`, or rewrite a real invariant as `it.prop` over a schema-derived arbitrary";
|
|
743
|
-
const meta$
|
|
812
|
+
const meta$21 = {
|
|
744
813
|
type: "problem",
|
|
745
814
|
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must contain only `it.prop` or `it.effect.prop` calls; every other test call fails — delete the block or rewrite the invariant as a property." },
|
|
746
815
|
schema: [],
|
|
747
|
-
messages: { nonPropCall: MESSAGE$
|
|
816
|
+
messages: { nonPropCall: MESSAGE$8 }
|
|
748
817
|
};
|
|
749
818
|
//#endregion
|
|
750
819
|
//#region src/rules/vitest-guard.ts
|
|
@@ -804,7 +873,7 @@ const rootNameOf = (node) => {
|
|
|
804
873
|
if (node.type === "CallExpression") return rootNameOf(node.callee);
|
|
805
874
|
};
|
|
806
875
|
const inSourceTestPropOnly = defineRule({
|
|
807
|
-
meta: meta$
|
|
876
|
+
meta: meta$21,
|
|
808
877
|
create(context) {
|
|
809
878
|
const filename = context.filename;
|
|
810
879
|
const basename = basenameOf(filename);
|
|
@@ -845,17 +914,17 @@ const NOT_MODULE_LEVEL_NAME = "a nested `import.meta.vitest` block";
|
|
|
845
914
|
const NOT_MODULE_LEVEL_EXPECTED = "the in-source test block as a direct statement of the module body";
|
|
846
915
|
const NOT_MODULE_LEVEL_ACTUAL = "an `import.meta.vitest` block nested inside another statement";
|
|
847
916
|
const NOT_MODULE_LEVEL_FIX = "move the block to module level — a nested block does not run under vitest includeSource";
|
|
848
|
-
const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no
|
|
849
|
-
const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a
|
|
850
|
-
const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
|
|
917
|
+
const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no module-level binding";
|
|
918
|
+
const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising this module's own code — a private helper, or a module-level function the lawful prop's `subject` names";
|
|
919
|
+
const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported-call or imported names";
|
|
851
920
|
const NO_PRIVATE_TARGET_FIX = "test the public surface from tests/ as *.integration.test.ts; in-source blocks exist for private helpers only — if the public behaviour you meant to cover is a pure function, delete the assertion: the type system already proves it";
|
|
852
|
-
const meta$
|
|
921
|
+
const meta$20 = {
|
|
853
922
|
type: "problem",
|
|
854
923
|
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise at least one non-exported binding; other tests belong in tests/." },
|
|
855
924
|
schema: [],
|
|
856
925
|
messages: {
|
|
857
|
-
notModuleLevel: MESSAGE$
|
|
858
|
-
noPrivateTarget: MESSAGE$
|
|
926
|
+
notModuleLevel: MESSAGE$8,
|
|
927
|
+
noPrivateTarget: MESSAGE$8
|
|
859
928
|
}
|
|
860
929
|
};
|
|
861
930
|
//#endregion
|
|
@@ -882,8 +951,25 @@ const collectPrivateNames = (body, out) => {
|
|
|
882
951
|
if (isCollectable(node)) collectDeclaration(node, out);
|
|
883
952
|
}
|
|
884
953
|
};
|
|
954
|
+
const collectModuleNames = (body, out) => {
|
|
955
|
+
for (const node of body) {
|
|
956
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
957
|
+
if (node.source === null && node.declaration !== null && isCollectable(node.declaration)) collectDeclaration(node.declaration, out);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (node.type === "ExportDefaultDeclaration" && isCollectable(node.declaration)) {
|
|
961
|
+
collectDeclaration(node.declaration, out);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (isCollectable(node)) collectDeclaration(node, out);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
const isSubjectValue = (node) => {
|
|
968
|
+
const parent = node.parent;
|
|
969
|
+
return parent.type === "Property" && parent.computed !== true && parent.key.type === "Identifier" && parent.key.name === "subject" && parent.value === node;
|
|
970
|
+
};
|
|
885
971
|
const inSourceTestTargetsPrivate = defineRule({
|
|
886
|
-
meta: meta$
|
|
972
|
+
meta: meta$20,
|
|
887
973
|
create(context) {
|
|
888
974
|
const filename = context.filename;
|
|
889
975
|
const basename = basenameOf(filename);
|
|
@@ -891,10 +977,12 @@ const inSourceTestTargetsPrivate = defineRule({
|
|
|
891
977
|
const inTestFile = isTestFile(basename);
|
|
892
978
|
if (!underSrc || inTestFile) return {};
|
|
893
979
|
const privateNames = /* @__PURE__ */ new Set();
|
|
980
|
+
const moduleNames = /* @__PURE__ */ new Set();
|
|
894
981
|
const guards = [];
|
|
895
982
|
return {
|
|
896
983
|
Program(node) {
|
|
897
984
|
collectPrivateNames(node.body, privateNames);
|
|
985
|
+
collectModuleNames(node.body, moduleNames);
|
|
898
986
|
},
|
|
899
987
|
IfStatement(node) {
|
|
900
988
|
if (!isVitestGuard(node.test)) return;
|
|
@@ -917,7 +1005,7 @@ const inSourceTestTargetsPrivate = defineRule({
|
|
|
917
1005
|
});
|
|
918
1006
|
},
|
|
919
1007
|
Identifier(node) {
|
|
920
|
-
if (!privateNames.has(node.name)) return;
|
|
1008
|
+
if (!(privateNames.has(node.name) || moduleNames.has(node.name) && isSubjectValue(node))) return;
|
|
921
1009
|
const guard = guards.find((g) => isInsideConsequent(node, g.node.consequent));
|
|
922
1010
|
if (guard !== void 0) guard.hit = true;
|
|
923
1011
|
},
|
|
@@ -940,6 +1028,122 @@ const inSourceTestTargetsPrivate = defineRule({
|
|
|
940
1028
|
}
|
|
941
1029
|
});
|
|
942
1030
|
//#endregion
|
|
1031
|
+
//#region src/rules/model-fixture-imports-subject.config.ts
|
|
1032
|
+
const SUBJECT_IMPORT_NAME = "a *.model.ts fixture that imports the package under test";
|
|
1033
|
+
const SUBJECT_IMPORT_EXPECTED = "a pure model that imports only effect, node builtins, and harness types";
|
|
1034
|
+
const SUBJECT_IMPORT_FIX = "delete the import; the model derives its state from the declared Schema it is given, so it needs no symbol from the package it stands in for";
|
|
1035
|
+
const RELATIVE_ESCAPE_NAME = "a *.model.ts fixture that climbs out of the tests tree";
|
|
1036
|
+
const RELATIVE_ESCAPE_EXPECTED = "a relative import that stays inside the tests tree — a sibling fixture under tests/__fixtures__/";
|
|
1037
|
+
const RELATIVE_ESCAPE_FIX = "import the sibling fixture with ./<name>.js, or move the model; a model that reaches src/ is the implementation checking itself";
|
|
1038
|
+
const meta$19 = {
|
|
1039
|
+
type: "problem",
|
|
1040
|
+
docs: { description: "A *.model.ts fixture is the pure model a conformance check stands the implementation against, so it may not import the package whose tests it serves. The package is derived from the path: the directory the tests tree lives in. An import, re-export, dynamic import, or require of that package name — or of any subpath of it — is the implementation reaching its own oracle. A relative specifier is judged the same way lexically: resolved against the fixture directory, it must stay inside the tests tree, so ../../src/Lock.js is the implementation again while ./Locks.js is a sibling fixture." },
|
|
1041
|
+
schema: [],
|
|
1042
|
+
messages: {
|
|
1043
|
+
subjectImport: MESSAGE$8,
|
|
1044
|
+
relativeEscape: MESSAGE$8
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/rules/model-fixture-imports-subject.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* The basename of the directory whose tests tree the fixture lives in. A
|
|
1051
|
+
* `*.model.ts` serves that package's tests, so the directory name is the
|
|
1052
|
+
* unscoped name of the package under test. Read lexically, the way every rule
|
|
1053
|
+
* here reads a path: one file cannot know what else is on disk (OX-TS2), and
|
|
1054
|
+
* with no tests tree in the path the subject is unknown — an unknown subject is
|
|
1055
|
+
* never reported.
|
|
1056
|
+
*/
|
|
1057
|
+
const packageDirOf = (filename) => {
|
|
1058
|
+
const dirs = directoriesOf(filename);
|
|
1059
|
+
const index = dirs.findIndex((segment) => TEST_TREE_DIRS.has(segment));
|
|
1060
|
+
return index <= 0 ? void 0 : dirs[index - 1];
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* The unscoped, subpath-stripped head of a package specifier:
|
|
1064
|
+
* `@systemfsoftware/effect-lock/testing` -> `effect-lock`. A subpath is still
|
|
1065
|
+
* the same dependency, and admitting it would let a fixture reach the
|
|
1066
|
+
* implementation through a nested entry.
|
|
1067
|
+
*/
|
|
1068
|
+
const packageHeadOf = (source) => {
|
|
1069
|
+
const unscoped = source.startsWith("@") ? source.slice(source.indexOf("/") + 1) : source;
|
|
1070
|
+
return unscoped.includes("/") ? unscoped.slice(0, unscoped.indexOf("/")) : unscoped;
|
|
1071
|
+
};
|
|
1072
|
+
const importsSubject = (source, subjectDir) => subjectDir !== void 0 && packageHeadOf(source) === subjectDir;
|
|
1073
|
+
const specifierOf$1 = (node) => node !== void 0 && node.type === "Literal" && typeof node.value === "string" ? node.value : void 0;
|
|
1074
|
+
const isRelativeSpecifier = (source) => source.startsWith("./") || source.startsWith("../");
|
|
1075
|
+
/**
|
|
1076
|
+
* The directory stack a relative specifier resolves to against the linted
|
|
1077
|
+
* file's own directory, `..` segments collapsed. Existence is never checked:
|
|
1078
|
+
* one file cannot know what else is on disk (OX-TS2), so only the lexical
|
|
1079
|
+
* identity is decided.
|
|
1080
|
+
*/
|
|
1081
|
+
const resolveRelative = (source, filename) => {
|
|
1082
|
+
const stack = [...directoriesOf(filename)];
|
|
1083
|
+
for (const segment of source.split("/")) {
|
|
1084
|
+
if (segment === "" || segment === ".") continue;
|
|
1085
|
+
if (segment === "..") {
|
|
1086
|
+
stack.pop();
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
stack.push(segment);
|
|
1090
|
+
}
|
|
1091
|
+
return stack;
|
|
1092
|
+
};
|
|
1093
|
+
const inTestsTree = (segments) => segments.some((segment) => TEST_TREE_DIRS.has(segment));
|
|
1094
|
+
const modelFixtureImportsSubject = defineRule({
|
|
1095
|
+
meta: meta$19,
|
|
1096
|
+
create(context) {
|
|
1097
|
+
if (!basenameOf(context.filename).endsWith(".model.ts")) return {};
|
|
1098
|
+
const subjectDir = packageDirOf(context.filename);
|
|
1099
|
+
const fixtureInTestsTree = inTestsTree(directoriesOf(context.filename));
|
|
1100
|
+
const checkSource = (node, source) => {
|
|
1101
|
+
const specifier = specifierOf$1(source);
|
|
1102
|
+
if (specifier === void 0) return;
|
|
1103
|
+
if (importsSubject(specifier, subjectDir)) {
|
|
1104
|
+
context.report({
|
|
1105
|
+
node,
|
|
1106
|
+
messageId: "subjectImport",
|
|
1107
|
+
data: {
|
|
1108
|
+
name: SUBJECT_IMPORT_NAME,
|
|
1109
|
+
expected: SUBJECT_IMPORT_EXPECTED,
|
|
1110
|
+
actual: `a dependency on ${specifier} in a *.model.ts fixture`,
|
|
1111
|
+
fix: SUBJECT_IMPORT_FIX
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (fixtureInTestsTree && isRelativeSpecifier(specifier) && !inTestsTree(resolveRelative(specifier, context.filename))) context.report({
|
|
1117
|
+
node,
|
|
1118
|
+
messageId: "relativeEscape",
|
|
1119
|
+
data: {
|
|
1120
|
+
name: RELATIVE_ESCAPE_NAME,
|
|
1121
|
+
expected: RELATIVE_ESCAPE_EXPECTED,
|
|
1122
|
+
actual: `a relative import of ${specifier}, resolving outside the tests tree`,
|
|
1123
|
+
fix: RELATIVE_ESCAPE_FIX
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
return {
|
|
1128
|
+
ImportDeclaration(node) {
|
|
1129
|
+
checkSource(node, node.source);
|
|
1130
|
+
},
|
|
1131
|
+
ExportNamedDeclaration(node) {
|
|
1132
|
+
if (node.source !== null) checkSource(node, node.source);
|
|
1133
|
+
},
|
|
1134
|
+
ExportAllDeclaration(node) {
|
|
1135
|
+
checkSource(node, node.source);
|
|
1136
|
+
},
|
|
1137
|
+
ImportExpression(node) {
|
|
1138
|
+
checkSource(node, node.source);
|
|
1139
|
+
},
|
|
1140
|
+
TSImportEqualsDeclaration(node) {
|
|
1141
|
+
if (node.moduleReference.type === "TSExternalModuleReference") checkSource(node, node.moduleReference.expression);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
//#endregion
|
|
943
1147
|
//#region src/rules/no-assert-in-property.config.ts
|
|
944
1148
|
const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
945
1149
|
const meta$18 = {
|
|
@@ -1019,6 +1223,20 @@ const noAssertInProperty = defineRule({
|
|
|
1019
1223
|
}
|
|
1020
1224
|
});
|
|
1021
1225
|
//#endregion
|
|
1226
|
+
//#region src/rules/expect-call.ts
|
|
1227
|
+
const EXPECT = "expect";
|
|
1228
|
+
const isExpectCallee = (callee) => callee.type === "Identifier" && callee.name === EXPECT;
|
|
1229
|
+
const expectCallOf = (node, acceptsMember) => {
|
|
1230
|
+
if (node.callee.type !== "MemberExpression") return void 0;
|
|
1231
|
+
let target = node.callee.object;
|
|
1232
|
+
while (target.type === "MemberExpression") {
|
|
1233
|
+
if (acceptsMember !== void 0 && acceptsMember(target) === false) return void 0;
|
|
1234
|
+
target = target.object;
|
|
1235
|
+
}
|
|
1236
|
+
if (target.type !== "CallExpression" || isExpectCallee(target.callee) === false) return void 0;
|
|
1237
|
+
return target;
|
|
1238
|
+
};
|
|
1239
|
+
//#endregion
|
|
1022
1240
|
//#region src/rules/no-behaviourless-assertion.config.ts
|
|
1023
1241
|
const TEST_FILE = /\.(test|spec)\.[cm]?tsx?$/;
|
|
1024
1242
|
const BEHAVIOUR_NODES = {
|
|
@@ -1430,7 +1648,7 @@ const meta$14 = {
|
|
|
1430
1648
|
type: "problem",
|
|
1431
1649
|
docs: { description: "A *.integration.test.ts Feature builder must configure at least one environment Layer (.withLayer or .withScenarioLayer) to enforce real integration boundaries under WGI-CLS1." },
|
|
1432
1650
|
schema: [],
|
|
1433
|
-
messages: { noLayerInFeature: MESSAGE$
|
|
1651
|
+
messages: { noLayerInFeature: MESSAGE$8 }
|
|
1434
1652
|
};
|
|
1435
1653
|
//#endregion
|
|
1436
1654
|
//#region src/rules/no-pseudo-gherkin-unit-tests.ts
|
|
@@ -1438,7 +1656,6 @@ const LAYER_METHODS = {
|
|
|
1438
1656
|
withLayer: true,
|
|
1439
1657
|
withScenarioLayer: true
|
|
1440
1658
|
};
|
|
1441
|
-
const isBehaviourTest = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
1442
1659
|
const hasLayerInChain = (callNode) => {
|
|
1443
1660
|
let current = callNode;
|
|
1444
1661
|
while (current.type === "CallExpression") {
|
|
@@ -1463,7 +1680,7 @@ const findRootFeatureCall = (callNode) => {
|
|
|
1463
1680
|
const noPseudoGherkinUnitTests = defineRule({
|
|
1464
1681
|
meta: meta$14,
|
|
1465
1682
|
create(context) {
|
|
1466
|
-
if (!
|
|
1683
|
+
if (!isBehaviourBasename(basenameOf(context.filename))) return {};
|
|
1467
1684
|
return { CallExpression(node) {
|
|
1468
1685
|
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
|
|
1469
1686
|
if (node.callee.property.name === "body") {
|
|
@@ -1488,7 +1705,7 @@ const noPseudoGherkinUnitTests = defineRule({
|
|
|
1488
1705
|
const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1489
1706
|
const meta$13 = {
|
|
1490
1707
|
type: "problem",
|
|
1491
|
-
docs: { description: "Property predicates (it.prop / it.effect.prop from @
|
|
1708
|
+
docs: { description: "Property predicates (it.prop / it.effect.prop from @systemfsoftware/vitest) must return a boolean verdict on every code path. fast-check counts undefined as success, so a bare return, a non-boolean return, or falling off the end of the body is a silent pass. Opaque values (identifiers, member expressions, calls) are trusted to be boolean; literals and operators are checked." },
|
|
1492
1709
|
schema: [],
|
|
1493
1710
|
messages: {
|
|
1494
1711
|
bareReturn: MESSAGE$5,
|
|
@@ -1668,9 +1885,9 @@ const meta$12 = {
|
|
|
1668
1885
|
docs: { description: "Under src/, the only sanctioned test file is a single-segment <stem>.workflow.property.test.ts inside a sanctioned test directory, plus the generated schema-laws.test.ts entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source import.meta.vitest block, and a public-surface test moves outside src/ as an integration test. The sanctioned directory list is the sanctionedDirs option, defaulting to the one directory this repo runs." },
|
|
1669
1886
|
schema: [Schema.toJsonSchemaDocument(Options$1).schema],
|
|
1670
1887
|
messages: {
|
|
1671
|
-
testFileInSrc: MESSAGE$
|
|
1672
|
-
schemaTestInSrc: MESSAGE$
|
|
1673
|
-
propertyTestOutsideTestsDir: MESSAGE$
|
|
1888
|
+
testFileInSrc: MESSAGE$8,
|
|
1889
|
+
schemaTestInSrc: MESSAGE$8,
|
|
1890
|
+
propertyTestOutsideTestsDir: MESSAGE$8
|
|
1674
1891
|
}
|
|
1675
1892
|
};
|
|
1676
1893
|
//#endregion
|
|
@@ -1725,7 +1942,8 @@ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
|
|
|
1725
1942
|
"∘",
|
|
1726
1943
|
"∩",
|
|
1727
1944
|
"∪",
|
|
1728
|
-
"⊥"
|
|
1945
|
+
"⊥",
|
|
1946
|
+
"⊨"
|
|
1729
1947
|
]);
|
|
1730
1948
|
const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
|
|
1731
1949
|
const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
|
|
@@ -1740,7 +1958,7 @@ const meta$11 = {
|
|
|
1740
1958
|
incompleteScope: "Expected: a bound variable after the quantifier \"{{symbol}}\" in scope segment \"{{scope}}\" (e.g., ∀x, ∀order, ∃e, →Shipped). A property quantifies over a named input drawn from a generator; name it. A lone quantifier binds nothing and specifies no domain.",
|
|
1741
1959
|
emptyDomain: "Expected: a non-empty PascalCase domain between the two underscores in \"{{actual}}\". If this isn't a universal invariant, delete the test. Otherwise name the thing under test, e.g., ∀x_DecodeEncode_=x.",
|
|
1742
1960
|
domainLeaksDAMP: "Expected: an invariant domain, not scenario language. Actual: domain \"{{domain}}\" contains \"{{word}}\" — that describes one case, not a universal law. Delete this test. It is not a property. Find the actual invariant and write that instead.",
|
|
1743
|
-
invalidPredicateSymbol: "Expected: a relation symbol (≡ ≠ = ≤ ≥ ∈ ⊆ ⊇ → ¬ ∘ ∩ ∪ ⊥) starting the last segment of \"{{actual}}\". Actual: it ends with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise relate the output: = / ≡ (roundtrip or oracle), ⊆ / ∈ (invariant), ≠ (distinctness), ⊥ (impossibility).",
|
|
1961
|
+
invalidPredicateSymbol: "Expected: a relation symbol (≡ ≠ = ≤ ≥ ∈ ⊆ ⊇ → ¬ ∘ ∩ ∪ ⊥ ⊨) starting the last segment of \"{{actual}}\". Actual: it ends with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise relate the output: = / ≡ (roundtrip or oracle), ⊆ / ∈ (invariant), ≠ (distinctness), ⊨ (satisfies a declared model), ⊥ (impossibility).",
|
|
1744
1962
|
incompletePredicate: "Expected: an operand after the relation symbol \"{{symbol}}\" in predicate \"{{predicate}}\", naming what the output is related to: =x / ≡Oracle (roundtrip or reference), ⊆Input / ∈Ignored (invariant), ≠Zero (distinctness), ⊥Cancellable (impossibility — name the outcome that cannot occur). A bare symbol relates the output to nothing and so asserts no property."
|
|
1745
1963
|
}
|
|
1746
1964
|
};
|
|
@@ -2540,6 +2758,16 @@ const NEUTRAL_METHODS = {
|
|
|
2540
2758
|
match: true,
|
|
2541
2759
|
gen: true
|
|
2542
2760
|
};
|
|
2761
|
+
/**
|
|
2762
|
+
* The fork's lawful `it.prop(name, { of, subject }, holds)` hands the function
|
|
2763
|
+
* under test to `holds` as its first parameter. Calling that parameter is
|
|
2764
|
+
* calling the subject, so it reaches this module's code by construction — even
|
|
2765
|
+
* when the parameter name would otherwise read as a codec accessor.
|
|
2766
|
+
*/
|
|
2767
|
+
const subjectParameterOf = (predicate) => {
|
|
2768
|
+
const first = predicate.params[0];
|
|
2769
|
+
return first !== void 0 && first.type === "Identifier" ? first.name : void 0;
|
|
2770
|
+
};
|
|
2543
2771
|
const isNode = (value) => value !== null && typeof value === "object" && "type" in value;
|
|
2544
2772
|
const mentionsImportMetaVitest = (value) => {
|
|
2545
2773
|
if (Array.isArray(value)) return value.some((item) => mentionsImportMetaVitest(item));
|
|
@@ -2551,9 +2779,9 @@ const mentionsImportMetaVitest = (value) => {
|
|
|
2551
2779
|
}
|
|
2552
2780
|
return false;
|
|
2553
2781
|
};
|
|
2554
|
-
const collectPredicateShape = (provenance, node, shape) => {
|
|
2782
|
+
const collectPredicateShape = (provenance, node, shape, subjectParameter) => {
|
|
2555
2783
|
if (Array.isArray(node)) {
|
|
2556
|
-
for (const item of node) collectPredicateShape(provenance, item, shape);
|
|
2784
|
+
for (const item of node) collectPredicateShape(provenance, item, shape, subjectParameter);
|
|
2557
2785
|
return;
|
|
2558
2786
|
}
|
|
2559
2787
|
if (!isNode(node)) return;
|
|
@@ -2571,12 +2799,12 @@ const collectPredicateShape = (provenance, node, shape) => {
|
|
|
2571
2799
|
if (provenance.classifyCall(name, node) === "domain" || receiverIsLocal) shape.callsDomainFunction = true;
|
|
2572
2800
|
}
|
|
2573
2801
|
} else if (callee.type === "Identifier") {
|
|
2574
|
-
if (CODEC_ACCESSORS[callee.name] !== true && NEUTRAL_METHODS[callee.name] !== true && provenance.classifyCall(callee.name, node) === "domain") shape.callsDomainFunction = true;
|
|
2802
|
+
if (subjectParameter !== void 0 && callee.name === subjectParameter || CODEC_ACCESSORS[callee.name] !== true && NEUTRAL_METHODS[callee.name] !== true && provenance.classifyCall(callee.name, node) === "domain") shape.callsDomainFunction = true;
|
|
2575
2803
|
}
|
|
2576
2804
|
}
|
|
2577
2805
|
for (const [key, child] of Object.entries(node)) {
|
|
2578
2806
|
if (key === "parent") continue;
|
|
2579
|
-
collectPredicateShape(provenance, child, shape);
|
|
2807
|
+
collectPredicateShape(provenance, child, shape, subjectParameter);
|
|
2580
2808
|
}
|
|
2581
2809
|
};
|
|
2582
2810
|
const checkPropCall = (provenance, context, call) => {
|
|
@@ -2586,7 +2814,7 @@ const checkPropCall = (provenance, context, call) => {
|
|
|
2586
2814
|
usesCompilerProbe: false,
|
|
2587
2815
|
callsDomainFunction: false
|
|
2588
2816
|
};
|
|
2589
|
-
collectPredicateShape(provenance, predicate.body, shape);
|
|
2817
|
+
collectPredicateShape(provenance, predicate.body, shape, subjectParameterOf(predicate));
|
|
2590
2818
|
if (shape.usesCompilerProbe) {
|
|
2591
2819
|
context.report({
|
|
2592
2820
|
node: call,
|
|
@@ -2681,7 +2909,7 @@ const PROPERTY_FILE_KIND = {
|
|
|
2681
2909
|
suffix: PROPERTY_TEST_SUFFIX,
|
|
2682
2910
|
plainExpected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
|
|
2683
2911
|
plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
|
|
2684
|
-
rawFcExpected: "it.prop(...) or it.effect.prop(...) from @
|
|
2912
|
+
rawFcExpected: "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest",
|
|
2685
2913
|
rawFcActualSuffix: "bypasses the vitest/Effect integration",
|
|
2686
2914
|
rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
|
|
2687
2915
|
};
|
|
@@ -2794,7 +3022,7 @@ const meta$6 = {
|
|
|
2794
3022
|
docs: { description: "A property test under src/ must be a single-segment <stem>.workflow.property.test.ts beside the <stem>.workflow.ts it covers; every other property-test basename is unsanctioned. A source file whose suffix names a cell listed in the cellsRequiringTest option must additionally carry an in-source vitest block; that list is empty by default, so the presence arm is opt-in per consumer." },
|
|
2795
3023
|
schema: [Schema.toJsonSchemaDocument(Options).schema],
|
|
2796
3024
|
messages: {
|
|
2797
|
-
unsanctionedCell: MESSAGE$
|
|
3025
|
+
unsanctionedCell: MESSAGE$8,
|
|
2798
3026
|
missingCellTest: ABSENCE_MESSAGE
|
|
2799
3027
|
}
|
|
2800
3028
|
};
|
|
@@ -2855,7 +3083,7 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
2855
3083
|
type: "problem",
|
|
2856
3084
|
docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
|
|
2857
3085
|
schema: [],
|
|
2858
|
-
messages: { strayTestFile: MESSAGE$
|
|
3086
|
+
messages: { strayTestFile: MESSAGE$8 }
|
|
2859
3087
|
},
|
|
2860
3088
|
create(context) {
|
|
2861
3089
|
if (isUnderSrc(context.filename)) return {};
|
|
@@ -2877,24 +3105,25 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
2877
3105
|
});
|
|
2878
3106
|
//#endregion
|
|
2879
3107
|
//#region src/rules/test-suffix-outside-src.config.ts
|
|
2880
|
-
const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.differential.test.ts, or *.
|
|
3108
|
+
const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.contract.test.ts, *.differential.test.ts, *.trace.test.ts, or *.conformance.test.ts";
|
|
2881
3109
|
const UNSANCTIONED_SUFFIX_ACTUAL = "another test suffix outside src/";
|
|
2882
|
-
const UNSANCTIONED_SUFFIX_FIX = "public surface -> *.integration.test.ts; parity -> *.differential.test.ts; span graph -> *.trace.test.ts; pure-cell property -> in-source; restated literal -> delete";
|
|
3110
|
+
const UNSANCTIONED_SUFFIX_FIX = "public surface -> *.integration.test.ts; real-oracle medium -> *.contract.test.ts (contract lane); parity -> *.differential.test.ts; span graph -> *.trace.test.ts; concurrent or stateful behaviour -> *.conformance.test.ts; pure-cell property -> in-source; restated literal -> delete";
|
|
2883
3111
|
//#endregion
|
|
2884
3112
|
//#region src/rules/test-suffix-outside-src.ts
|
|
2885
3113
|
const testSuffixOutsideSrc = defineRule({
|
|
2886
3114
|
meta: {
|
|
2887
3115
|
type: "problem",
|
|
2888
|
-
docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, or .
|
|
3116
|
+
docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, .trace.test.ts, or .conformance.test.ts. A runner-driving package (Vitest itself, the fork, effect-spec-runtime) keeps plain *.test.ts runners and nested-run probes, so the lane taxonomy does not apply there." },
|
|
2889
3117
|
schema: [],
|
|
2890
|
-
messages: { unsanctionedSuffix: MESSAGE$
|
|
3118
|
+
messages: { unsanctionedSuffix: MESSAGE$8 }
|
|
2891
3119
|
},
|
|
2892
3120
|
create(context) {
|
|
2893
3121
|
const filename = context.filename;
|
|
2894
3122
|
if (isUnderSrc(filename)) return {};
|
|
3123
|
+
if (isRawVitestPackage(filename)) return {};
|
|
2895
3124
|
const basename = basenameOf(filename);
|
|
2896
3125
|
if (!isTestFile(basename)) return {};
|
|
2897
|
-
if (basename.endsWith(".
|
|
3126
|
+
if (isBehaviourBasename(basename) || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts") || basename.endsWith(".conformance.test.ts")) return {};
|
|
2898
3127
|
return { Program(node) {
|
|
2899
3128
|
context.report({
|
|
2900
3129
|
node,
|
|
@@ -2921,7 +3150,7 @@ const testsDirHelpersInFixtures = defineRule({
|
|
|
2921
3150
|
type: "problem",
|
|
2922
3151
|
docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
|
|
2923
3152
|
schema: [],
|
|
2924
|
-
messages: { helperOutsideFixtures: MESSAGE$
|
|
3153
|
+
messages: { helperOutsideFixtures: MESSAGE$8 }
|
|
2925
3154
|
},
|
|
2926
3155
|
create(context) {
|
|
2927
3156
|
const filename = context.filename;
|
|
@@ -2954,7 +3183,7 @@ const meta$2 = {
|
|
|
2954
3183
|
type: "problem",
|
|
2955
3184
|
docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
|
|
2956
3185
|
schema: [],
|
|
2957
|
-
messages: { sourceReachIn: MESSAGE$
|
|
3186
|
+
messages: { sourceReachIn: MESSAGE$8 }
|
|
2958
3187
|
};
|
|
2959
3188
|
//#endregion
|
|
2960
3189
|
//#region src/rules/tests-import-public-api.ts
|
|
@@ -3035,9 +3264,9 @@ const meta$1 = {
|
|
|
3035
3264
|
docs: { description: "A *.trace.test.ts must import its harness from @systemfsoftware/trace-spec and terminate cases on relation holds over the declared span graph. Raw emit calls (startSpan, spanBuilder, startActiveSpan) and HTTP status, statusText, or body assertions (expect(res.status).toBe(200), expect(res).toHaveProperty(\"status\", 200), expect(res).resolves.toMatchObject({ status: 200 }), expect(res).toEqual(expect.objectContaining({ status: 200 }))) are forbidden — replacing that altitude is what a trace spec exists for." },
|
|
3036
3265
|
schema: [],
|
|
3037
3266
|
messages: {
|
|
3038
|
-
missingHarnessImport: MESSAGE$
|
|
3039
|
-
httpTermination: MESSAGE$
|
|
3040
|
-
rawEmitCall: MESSAGE$
|
|
3267
|
+
missingHarnessImport: MESSAGE$8,
|
|
3268
|
+
httpTermination: MESSAGE$8,
|
|
3269
|
+
rawEmitCall: MESSAGE$8
|
|
3041
3270
|
}
|
|
3042
3271
|
};
|
|
3043
3272
|
//#endregion
|
|
@@ -3233,27 +3462,27 @@ const traceTestRequiresTaxonomy = defineRule({
|
|
|
3233
3462
|
}
|
|
3234
3463
|
});
|
|
3235
3464
|
//#endregion
|
|
3236
|
-
//#region src/rules/vitest-from-
|
|
3465
|
+
//#region src/rules/vitest-from-systemfsoftware-vitest.config.ts
|
|
3237
3466
|
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
3238
|
-
const VIOLATION_NAME = "a value import from vitest";
|
|
3239
|
-
const VIOLATION_EXPECTED = "every test value imported from @
|
|
3240
|
-
const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest";
|
|
3241
|
-
const VIOLATION_FIX = "import it from @
|
|
3467
|
+
const VIOLATION_NAME = "a value import from a foreign vitest runner";
|
|
3468
|
+
const VIOLATION_EXPECTED = "every test value imported from @systemfsoftware/vitest, which re-exports all of Vitest";
|
|
3469
|
+
const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest or @effect/vitest";
|
|
3470
|
+
const VIOLATION_FIX = "import it from @systemfsoftware/vitest instead of vitest or @effect/vitest";
|
|
3242
3471
|
const meta = {
|
|
3243
3472
|
type: "problem",
|
|
3244
|
-
docs: { description: "Every value a test uses from Vitest is imported from @
|
|
3473
|
+
docs: { description: "Every value a test uses from Vitest is imported from @systemfsoftware/vitest, which re-exports all of Vitest. Any value import from vitest or the upstream @effect/vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal." },
|
|
3245
3474
|
schema: [],
|
|
3246
3475
|
messages: { vitestImport: MESSAGE }
|
|
3247
3476
|
};
|
|
3248
3477
|
//#endregion
|
|
3249
|
-
//#region src/rules/vitest-from-
|
|
3478
|
+
//#region src/rules/vitest-from-systemfsoftware-vitest.ts
|
|
3250
3479
|
const isTypeOnly = (node) => {
|
|
3251
3480
|
if (node.importKind === "type") return true;
|
|
3252
3481
|
if (node.specifiers.length === 0) return false;
|
|
3253
3482
|
return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
3254
3483
|
};
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3484
|
+
const isForeignVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && FOREIGN_VITEST_SOURCES[node.value] === true;
|
|
3485
|
+
const vitestFromSystemfsoftwareVitest = defineRule({
|
|
3257
3486
|
meta,
|
|
3258
3487
|
create(context) {
|
|
3259
3488
|
if (isRawVitestPackage(context.filename)) return {};
|
|
@@ -3271,12 +3500,22 @@ const vitestFromEffectVitest = defineRule({
|
|
|
3271
3500
|
};
|
|
3272
3501
|
return {
|
|
3273
3502
|
ImportDeclaration(node) {
|
|
3274
|
-
if (node.source.value !==
|
|
3503
|
+
if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
|
|
3275
3504
|
if (isTypeOnly(node)) return;
|
|
3276
3505
|
report(node);
|
|
3277
3506
|
},
|
|
3278
3507
|
ImportExpression(node) {
|
|
3279
|
-
if (!
|
|
3508
|
+
if (!isForeignVitestLiteral(node.source)) return;
|
|
3509
|
+
report(node);
|
|
3510
|
+
},
|
|
3511
|
+
ExportAllDeclaration(node) {
|
|
3512
|
+
if (node.exportKind === "type") return;
|
|
3513
|
+
if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
|
|
3514
|
+
report(node);
|
|
3515
|
+
},
|
|
3516
|
+
ExportNamedDeclaration(node) {
|
|
3517
|
+
if (node.source === null || node.exportKind === "type") return;
|
|
3518
|
+
if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
|
|
3280
3519
|
report(node);
|
|
3281
3520
|
}
|
|
3282
3521
|
};
|
|
@@ -3307,11 +3546,12 @@ const recommendedRules = {
|
|
|
3307
3546
|
[rule("behaviour-exercises-use-case")]: "error",
|
|
3308
3547
|
[rule("behaviour-one-feature-per-file")]: "error",
|
|
3309
3548
|
[rule("tests-dir-helpers-in-fixtures")]: "error",
|
|
3310
|
-
[rule("
|
|
3311
|
-
[rule("vitest-from-effect-vitest")]: "error",
|
|
3549
|
+
[rule("vitest-from-systemfsoftware-vitest")]: "error",
|
|
3312
3550
|
[rule("no-io-module-in-source-test")]: "error",
|
|
3313
3551
|
[rule("tests-import-public-api")]: "error",
|
|
3314
3552
|
[rule("differential-test-requires-harness")]: "error",
|
|
3553
|
+
[rule("conformance-test-requires-harness")]: "error",
|
|
3554
|
+
[rule("model-fixture-imports-subject")]: "error",
|
|
3315
3555
|
[rule("no-pseudo-gherkin-unit-tests")]: "error",
|
|
3316
3556
|
[rule("ban-raw-span-name-emit")]: "error",
|
|
3317
3557
|
[rule("trace-test-requires-taxonomy")]: "error"
|
|
@@ -3340,11 +3580,12 @@ var src_default = {
|
|
|
3340
3580
|
"behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
|
|
3341
3581
|
"no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
|
|
3342
3582
|
"tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
|
|
3343
|
-
"
|
|
3344
|
-
"vitest-from-effect-vitest": vitestFromEffectVitest,
|
|
3583
|
+
"vitest-from-systemfsoftware-vitest": vitestFromSystemfsoftwareVitest,
|
|
3345
3584
|
"no-io-module-in-source-test": noIoModuleInSourceTest,
|
|
3346
3585
|
"tests-import-public-api": testsImportPublicApi,
|
|
3347
3586
|
"differential-test-requires-harness": differentialTestRequiresHarness,
|
|
3587
|
+
"conformance-test-requires-harness": conformanceTestRequiresHarness,
|
|
3588
|
+
"model-fixture-imports-subject": modelFixtureImportsSubject,
|
|
3348
3589
|
"ban-raw-span-name-emit": banRawSpanNameEmit,
|
|
3349
3590
|
"trace-test-requires-taxonomy": traceTestRequiresTaxonomy
|
|
3350
3591
|
},
|
|
@@ -26,11 +26,12 @@ declare const _default: {
|
|
|
26
26
|
'behaviour-one-feature-per-file': Rule;
|
|
27
27
|
'no-pseudo-gherkin-unit-tests': Rule;
|
|
28
28
|
'tests-dir-helpers-in-fixtures': Rule;
|
|
29
|
-
'
|
|
30
|
-
'vitest-from-effect-vitest': Rule;
|
|
29
|
+
'vitest-from-systemfsoftware-vitest': Rule;
|
|
31
30
|
'no-io-module-in-source-test': Rule;
|
|
32
31
|
'tests-import-public-api': Rule;
|
|
33
32
|
'differential-test-requires-harness': Rule;
|
|
33
|
+
'conformance-test-requires-harness': Rule;
|
|
34
|
+
'model-fixture-imports-subject': Rule;
|
|
34
35
|
'ban-raw-span-name-emit': Rule;
|
|
35
36
|
'trace-test-requires-taxonomy': Rule;
|
|
36
37
|
};
|
|
@@ -26,11 +26,12 @@ declare const _default: {
|
|
|
26
26
|
'behaviour-one-feature-per-file': Rule;
|
|
27
27
|
'no-pseudo-gherkin-unit-tests': Rule;
|
|
28
28
|
'tests-dir-helpers-in-fixtures': Rule;
|
|
29
|
-
'
|
|
30
|
-
'vitest-from-effect-vitest': Rule;
|
|
29
|
+
'vitest-from-systemfsoftware-vitest': Rule;
|
|
31
30
|
'no-io-module-in-source-test': Rule;
|
|
32
31
|
'tests-import-public-api': Rule;
|
|
33
32
|
'differential-test-requires-harness': Rule;
|
|
33
|
+
'conformance-test-requires-harness': Rule;
|
|
34
|
+
'model-fixture-imports-subject': Rule;
|
|
34
35
|
'ban-raw-span-name-emit': Rule;
|
|
35
36
|
'trace-test-requires-taxonomy': Rule;
|
|
36
37
|
};
|