@shivaedev/quality 0.29.1 → 0.31.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 +23 -0
- package/README.md +11 -7
- package/dist/domEnvironment.config.js +1 -1
- package/dist/domEnvironment.config.js.map +1 -1
- package/dist/imports/fences/reach.d.ts +9 -0
- package/dist/imports/fences/reach.d.ts.map +1 -1
- package/dist/imports/fences/reach.js +5 -2
- package/dist/imports/fences/reach.js.map +1 -1
- package/dist/naming/testName.d.ts +1 -1
- package/dist/naming/testName.d.ts.map +1 -1
- package/dist/naming/testName.js +1 -1
- package/dist/naming/testName.js.map +1 -1
- package/dist/rules/biome.d.ts +1 -1
- package/dist/rules/biome.d.ts.map +1 -1
- package/dist/rules/biome.js +1 -1
- package/dist/rules/biome.js.map +1 -1
- package/dist/rules/builtIn.d.ts +1 -1
- package/dist/rules/builtIn.d.ts.map +1 -1
- package/dist/rules/builtIn.js +2 -0
- package/dist/rules/builtIn.js.map +1 -1
- package/dist/rules/manifestsExportsShape.d.ts +2 -0
- package/dist/rules/manifestsExportsShape.d.ts.map +1 -0
- package/dist/rules/manifestsExportsShape.js +65 -0
- package/dist/rules/manifestsExportsShape.js.map +1 -0
- package/dist/rules/stories/engineOf.d.ts +3 -0
- package/dist/rules/stories/engineOf.d.ts.map +1 -0
- package/dist/rules/stories/engineOf.js +62 -0
- package/dist/rules/stories/engineOf.js.map +1 -0
- package/dist/rules/stories/origins.js +2 -2
- package/dist/rules/stories/origins.js.map +1 -1
- package/dist/rules/stories/specs.d.ts.map +1 -1
- package/dist/rules/stories/specs.js +7 -2
- package/dist/rules/stories/specs.js.map +1 -1
- package/dist/rules/stories/unitSyntax.d.ts +8 -0
- package/dist/rules/stories/unitSyntax.d.ts.map +1 -0
- package/dist/rules/stories/unitSyntax.js +77 -0
- package/dist/rules/stories/unitSyntax.js.map +1 -0
- package/dist/rules/stories/unitTests.d.ts +4 -0
- package/dist/rules/stories/unitTests.d.ts.map +1 -0
- package/dist/rules/stories/unitTests.js +76 -0
- package/dist/rules/stories/unitTests.js.map +1 -0
- package/dist/rules/test-names/colocated.d.ts.map +1 -1
- package/dist/rules/test-names/colocated.js +2 -1
- package/dist/rules/test-names/colocated.js.map +1 -1
- package/dist/rules/test-names/follow.d.ts.map +1 -1
- package/dist/rules/test-names/follow.js +16 -6
- package/dist/rules/test-names/follow.js.map +1 -1
- package/package.json +2 -2
- package/src/domEnvironment.config.ts +1 -1
- package/src/imports/fences/reach.ts +12 -3
- package/src/naming/testName.ts +1 -1
- package/src/rules/biome.ts +1 -1
- package/src/rules/builtIn.ts +2 -0
- package/src/rules/manifestsExportsShape.ts +79 -0
- package/src/rules/stories/engineOf.ts +65 -0
- package/src/rules/stories/origins.ts +2 -2
- package/src/rules/stories/specs.ts +7 -3
- package/src/rules/stories/unitSyntax.ts +95 -0
- package/src/rules/stories/unitTests.ts +86 -0
- package/src/rules/test-names/colocated.ts +2 -1
- package/src/rules/test-names/follow.ts +17 -6
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const MOCKS = new Set([
|
|
3
|
+
"doMock",
|
|
4
|
+
"fn",
|
|
5
|
+
"hoisted",
|
|
6
|
+
"importActual",
|
|
7
|
+
"importMock",
|
|
8
|
+
"mock",
|
|
9
|
+
"mocked",
|
|
10
|
+
"mockObject",
|
|
11
|
+
"setSystemTime",
|
|
12
|
+
"spyOn",
|
|
13
|
+
"stubEnv",
|
|
14
|
+
"stubGlobal",
|
|
15
|
+
"useFakeTimers",
|
|
16
|
+
]);
|
|
17
|
+
const VITEST = new Set(["vitest", "@effect/vitest"]);
|
|
18
|
+
const EFFECT_MODULE = /^effect\/(?<name>[A-Z]\w*)$/u;
|
|
19
|
+
function bindingOf(module, imported) {
|
|
20
|
+
const name = EFFECT_MODULE.exec(module)?.groups?.name;
|
|
21
|
+
return name === undefined ? { module, path: imported } : { module: "effect", path: [name, ...imported] };
|
|
22
|
+
}
|
|
23
|
+
function importsOf(source) {
|
|
24
|
+
const bound = new Map();
|
|
25
|
+
for (const statement of source.statements) {
|
|
26
|
+
if (!(ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier))) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const module = statement.moduleSpecifier.text;
|
|
30
|
+
const bindings = statement.importClause?.namedBindings;
|
|
31
|
+
if (bindings !== undefined && ts.isNamespaceImport(bindings)) {
|
|
32
|
+
bound.set(bindings.name.text, bindingOf(module, []));
|
|
33
|
+
}
|
|
34
|
+
for (const element of bindings !== undefined && ts.isNamedImports(bindings) ? bindings.elements : []) {
|
|
35
|
+
bound.set(element.name.text, bindingOf(module, [(element.propertyName ?? element.name).text]));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return bound;
|
|
39
|
+
}
|
|
40
|
+
function qualified(node, bound) {
|
|
41
|
+
if (ts.isIdentifier(node)) {
|
|
42
|
+
return bound.get(node.text) ?? (node.text === "vi" ? { module: "vitest", path: ["vi"] } : undefined);
|
|
43
|
+
}
|
|
44
|
+
const receiver = ts.isPropertyAccessExpression(node) ? qualified(node.expression, bound) : undefined;
|
|
45
|
+
return receiver === undefined || !ts.isPropertyAccessExpression(node) ? undefined : { ...receiver, path: [...receiver.path, node.name.text] };
|
|
46
|
+
}
|
|
47
|
+
function standOf({ module, path }) {
|
|
48
|
+
const [head = "", member = "", ...rest] = path;
|
|
49
|
+
if (rest.length > 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (VITEST.has(module) && head === "vi" && MOCKS.has(member)) {
|
|
53
|
+
return "mocks";
|
|
54
|
+
}
|
|
55
|
+
if (module === "@effect/vitest" && ((head === "layer" && member === "") || (head === "it" && member === "layer"))) {
|
|
56
|
+
return "provides";
|
|
57
|
+
}
|
|
58
|
+
const provides = (head === "Effect" && member.startsWith("provide")) || ((head === "Layer" || head === "ManagedRuntime") && member !== "");
|
|
59
|
+
return module === "effect" && provides ? "provides" : undefined;
|
|
60
|
+
}
|
|
61
|
+
export function standIns(source) {
|
|
62
|
+
const bound = importsOf(source);
|
|
63
|
+
const found = new Map();
|
|
64
|
+
function visit(node) {
|
|
65
|
+
const called = ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node;
|
|
66
|
+
const name = ts.isPropertyAccessExpression(node) || (called && ts.isIdentifier(node)) ? qualified(node, bound) : undefined;
|
|
67
|
+
const why = name === undefined ? undefined : standOf(name);
|
|
68
|
+
const key = name === undefined ? "" : `${name.module}#${name.path.join(".")}`;
|
|
69
|
+
if (why !== undefined && !found.has(key)) {
|
|
70
|
+
found.set(key, { line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1, what: node.getText(source), why });
|
|
71
|
+
}
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
}
|
|
74
|
+
visit(source);
|
|
75
|
+
return [...found.values()];
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=unitSyntax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitSyntax.js","sourceRoot":"","sources":["../../../src/rules/stories/unitSyntax.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
2
|
+
import type { Finding, SourceFile } from "#rule.ts";
|
|
3
|
+
export declare function unitTests(graph: ImportGraph, sources: readonly SourceFile[]): (file: string) => readonly Finding[];
|
|
4
|
+
//# sourceMappingURL=unitTests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitTests.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/unitTests.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGjE,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAsEpD,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,OAAO,EAAE,CAWlH"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { labelOf, outgoing, walkFrom } from "#imports/fences/reach.ts";
|
|
2
|
+
import { packageNameOf } from "#imports/resolve.ts";
|
|
3
|
+
import { isTestSupport } from "#naming/testName.ts";
|
|
4
|
+
import { parse } from "#rules/syntax.ts";
|
|
5
|
+
import { usesDom } from "#rules/test-names/usesDom.ts";
|
|
6
|
+
import { engineOf } from "./engineOf.js";
|
|
7
|
+
import { standIns } from "./unitSyntax.js";
|
|
8
|
+
const RELATIVE = /^[./#]/u;
|
|
9
|
+
const KIT = "Drive the engine through the package's story kit in a story, or, if the code under test is pure,";
|
|
10
|
+
function runtimeEdges(graph) {
|
|
11
|
+
const packages = graph.unresolved
|
|
12
|
+
.filter((request) => request.missing === undefined && !RELATIVE.test(request.specifier))
|
|
13
|
+
.map((request) => ({ ...request, to: { kind: "external", package: packageNameOf(request.specifier), specifier: request.specifier } }));
|
|
14
|
+
return outgoing([...graph.edges, ...packages].filter((edge) => edge.kind === "import" && !edge.type));
|
|
15
|
+
}
|
|
16
|
+
function repair(path) {
|
|
17
|
+
const [, first, ...rest] = path;
|
|
18
|
+
if (first === undefined || rest.length === 0) {
|
|
19
|
+
return "remove that import from the .unit file.";
|
|
20
|
+
}
|
|
21
|
+
return isTestSupport(first)
|
|
22
|
+
? `give the .unit file fixtures from a test-support module that reaches no engine instead of ${first}.`
|
|
23
|
+
: `split ${first}: move the pure code under test into its own module that reaches no engine, and test that module in the .unit file.`;
|
|
24
|
+
}
|
|
25
|
+
function reachFinding(file, reached) {
|
|
26
|
+
return {
|
|
27
|
+
file,
|
|
28
|
+
line: reached.line,
|
|
29
|
+
message: `Is a unit test but reaches ${engineOf(reached.target) ?? "an engine"} (${labelOf(reached.target)}) through ${reached.path.join(" -> ")}. A unit test reaches no engine, directly or through what it imports. ${KIT} ${repair(reached.path)}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function standMessage(stand) {
|
|
33
|
+
return stand.why === "mocks"
|
|
34
|
+
? `mocks with ${stand.what}. A unit test calls real code with real values and stands nothing in for a collaborator.`
|
|
35
|
+
: `provides services with ${stand.what}. A unit test runs only requirement-free code, such as Effect.runSync on an Effect that needs nothing.`;
|
|
36
|
+
}
|
|
37
|
+
function where(file, module, trail) {
|
|
38
|
+
return module === file ? "" : ` in ${module}, which it imports through ${trail.path.join(" -> ")},`;
|
|
39
|
+
}
|
|
40
|
+
function syntaxFindings(file, module, trail, source) {
|
|
41
|
+
const syntax = source === undefined ? undefined : parse(source);
|
|
42
|
+
if (source === undefined || syntax === undefined) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
function line(own) {
|
|
46
|
+
return module === file ? own : trail.line;
|
|
47
|
+
}
|
|
48
|
+
const stands = standIns(syntax).map((stand) => ({
|
|
49
|
+
file,
|
|
50
|
+
line: line(stand.line),
|
|
51
|
+
message: `Is a unit test but${where(file, module, trail)} ${standMessage(stand)} ${KIT} test it in the .unit file without stand-ins.`,
|
|
52
|
+
}));
|
|
53
|
+
const dom = usesDom(source)
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
file,
|
|
57
|
+
line: line(1),
|
|
58
|
+
message: `Is a unit test but${where(file, module, trail)} uses the DOM. A unit test runs without one. ${KIT} test it in the .unit file without the DOM.`,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: [];
|
|
62
|
+
return [...stands, ...dom];
|
|
63
|
+
}
|
|
64
|
+
export function unitTests(graph, sources) {
|
|
65
|
+
const edgesFrom = runtimeEdges(graph);
|
|
66
|
+
const byPath = new Map(sources.map((source) => [source.path, source]));
|
|
67
|
+
return (file) => {
|
|
68
|
+
const walked = walkFrom(file, edgesFrom, (endpoint) => engineOf(endpoint) !== undefined);
|
|
69
|
+
const checked = [...walked.visited].filter(([module]) => module === file || isTestSupport(module));
|
|
70
|
+
return [
|
|
71
|
+
...[...walked.reached].sort((a, b) => a.line - b.line).map((reached) => reachFinding(file, reached)),
|
|
72
|
+
...checked.flatMap(([module, trail]) => syntaxFindings(file, module, trail, byPath.get(module))),
|
|
73
|
+
];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=unitTests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitTests.js","sourceRoot":"","sources":["../../../src/rules/stories/unitTests.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"colocated.d.ts","sourceRoot":"","sources":["../../../src/rules/test-names/colocated.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"colocated.d.ts","sourceRoot":"","sources":["../../../src/rules/test-names/colocated.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,cAAc;;EAQzB,CAAC"}
|
|
@@ -12,10 +12,11 @@ function findingOf(path) {
|
|
|
12
12
|
if (name === undefined || mirror === undefined) {
|
|
13
13
|
return [];
|
|
14
14
|
}
|
|
15
|
+
const modifiers = name.modifiers.map((modifier) => `.${modifier}`).join("");
|
|
15
16
|
return [
|
|
16
17
|
{
|
|
17
18
|
file: path,
|
|
18
|
-
message: `Sits in the test folder "${mirror}/". A test sits beside the file it covers, as ${name.stem}.test${name.extension} beside ${name.stem}.ts, or as a <behaviour
|
|
19
|
+
message: `Sits in the test folder "${mirror}/". A test sits beside the file it covers, as ${name.stem}${modifiers}.test${name.extension} beside ${name.stem}.ts, or as a <behaviour>${modifiers}.spec${name.extension} in the folder it covers. A folder of tests that cover several packages is a suite in the "suites" option.`,
|
|
19
20
|
},
|
|
20
21
|
];
|
|
21
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"colocated.js","sourceRoot":"","sources":["../../../src/rules/test-names/colocated.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAgB,MAAM,UAAU,CAAC;AAEpD,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5F,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;CAC3F,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAY;IAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACpF,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,EAAE,CAAC;IACX,CAAC;IACD,OAAO;QACN;YACC,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,4BAA4B,MAAM,iDAAiD,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"colocated.js","sourceRoot":"","sources":["../../../src/rules/test-names/colocated.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAgB,MAAM,UAAU,CAAC;AAEpD,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5F,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;CAC3F,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAY;IAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACpF,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,OAAO;QACN;YACC,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,4BAA4B,MAAM,iDAAiD,IAAI,CAAC,IAAI,GAAG,SAAS,QAAQ,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,IAAI,2BAA2B,SAAS,QAAQ,IAAI,CAAC,SAAS,4GAA4G;SACjU;KACD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;IACxC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACjD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACzE,CAAC;IACD,WAAW,EAAE,gIAAgI;IAC7I,EAAE,EAAE,iBAAiB;IACrB,OAAO,EAAE,MAAM,CAAC,kBAAkB,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;CACzH,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"follow.d.ts","sourceRoot":"","sources":["../../../src/rules/test-names/follow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"follow.d.ts","sourceRoot":"","sources":["../../../src/rules/test-names/follow.ts"],"names":[],"mappings":"AA8EA,eAAO,MAAM,WAAW;;EAUtB,CAAC"}
|
|
@@ -8,7 +8,7 @@ import { usesDom } from "#rules/test-names/usesDom.ts";
|
|
|
8
8
|
const ENVIRONMENT_NAMES = new Set(ENVIRONMENTS);
|
|
9
9
|
const FIXTURE_SUFFIXES = new Set(["integration", "postgres"]);
|
|
10
10
|
const CODE = /\.[cm]?[jt]sx?$/u;
|
|
11
|
-
const GRAMMAR = "A test file is <file>[.<
|
|
11
|
+
const GRAMMAR = "A test file is <file>[.<modifier>].test.ts beside its file or <behaviour>[.<modifier>].spec.ts in the folder it covers, with at most one modifier of dom, slow, typecheck and unit.";
|
|
12
12
|
const TestsFollowOptions = Schema.Struct({
|
|
13
13
|
suites: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
14
14
|
});
|
|
@@ -20,29 +20,39 @@ function suffixMessage(file, name) {
|
|
|
20
20
|
if (first === undefined || (rest.length === 0 && ENVIRONMENT_NAMES.has(first))) {
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
+
if (name.modifiers.includes("unit") && name.modifiers.includes("dom")) {
|
|
24
|
+
return `"${file}" is both a unit and a DOM test, but a unit test runs without the DOM. Drive the DOM through the package's story kit in ${name.stem}.dom.${name.kind}${name.extension}, or keep only pure code in ${name.stem}.unit.${name.kind}${name.extension}.`;
|
|
25
|
+
}
|
|
23
26
|
const fixture = name.modifiers.find((modifier) => FIXTURE_SUFFIXES.has(modifier));
|
|
24
27
|
if (fixture !== undefined) {
|
|
25
28
|
return `".${fixture}" is not a test environment. A test that needs a database or a running app takes it from the app's test fixture and is named like any other test. ${GRAMMAR}`;
|
|
26
29
|
}
|
|
27
30
|
return `"${file}" has the suffixes ".${name.modifiers.join(".")}". ${GRAMMAR} An aspect of a module gets its own file in the module's folder, or a .spec.`;
|
|
28
31
|
}
|
|
32
|
+
function suffixOf(name) {
|
|
33
|
+
const [only, ...rest] = name.modifiers;
|
|
34
|
+
return only !== undefined && rest.length === 0 && ENVIRONMENT_NAMES.has(only) ? `.${only}` : "";
|
|
35
|
+
}
|
|
29
36
|
function placeMessage(file, name, siblings) {
|
|
30
37
|
const covered = siblings.has(`${name.folder}/${name.stem}`);
|
|
38
|
+
const test = `${suffixOf(name)}.test${name.extension}`;
|
|
39
|
+
const spec = `checkoutFlow${suffixOf(name)}.spec${name.extension}`;
|
|
31
40
|
if (name.kind === "test") {
|
|
32
41
|
return covered
|
|
33
42
|
? undefined
|
|
34
|
-
: `"${file}" follows no file: ${name.stem}
|
|
43
|
+
: `"${file}" follows no file: ${name.stem}${test} sits beside ${name.stem}.ts in the same folder. A test that covers the folder as a whole is a <behaviour>.spec${name.extension}, such as ${spec}.`;
|
|
35
44
|
}
|
|
36
45
|
if (covered) {
|
|
37
|
-
return `"${file}" shares its name with ${name.stem}.ts. A .spec names a behaviour of its folder, such as
|
|
46
|
+
return `"${file}" shares its name with ${name.stem}.ts. A .spec names a behaviour of its folder, such as ${spec}; a test of ${name.stem}.ts is ${name.stem}${test}.`;
|
|
38
47
|
}
|
|
39
|
-
return CAMEL.test(name.stem) ? undefined : `"${file}" names its behaviour in camelCase, such as
|
|
48
|
+
return CAMEL.test(name.stem) ? undefined : `"${file}" names its behaviour in camelCase, such as ${spec}.`;
|
|
40
49
|
}
|
|
41
50
|
function environmentMessage(file, name, source) {
|
|
42
51
|
if (name.modifiers.includes("dom") || name.modifiers.includes("typecheck") || !usesDom(source)) {
|
|
43
52
|
return undefined;
|
|
44
53
|
}
|
|
45
|
-
|
|
54
|
+
const unit = name.modifiers.includes("unit") ? ", not a unit test, and drives the DOM through the package's story kit" : "";
|
|
55
|
+
return `"${file}" uses the DOM, so it is ${name.stem}.dom.${name.kind}${name.extension}${unit} and runs in the dom project.`;
|
|
46
56
|
}
|
|
47
57
|
function findingsOf(source, siblings) {
|
|
48
58
|
const name = testName(source.path);
|
|
@@ -59,7 +69,7 @@ export const testsFollow = defineRule({
|
|
|
59
69
|
const siblings = siblingsIn(files);
|
|
60
70
|
return sources.filter((source) => !suites.ignores(source.path)).flatMap((source) => findingsOf(source, siblings));
|
|
61
71
|
},
|
|
62
|
-
description: "A .test file follows the file beside it, a .spec names a behaviour of its folder, the only suffix is one
|
|
72
|
+
description: "A .test file follows the file beside it, a .spec names a behaviour of its folder, the only suffix is one modifier of dom, slow, typecheck and unit, and a test that uses the DOM is a .dom test.",
|
|
63
73
|
id: "tests/follow",
|
|
64
74
|
options: Schema.toStandardSchemaV1(TestsFollowOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
65
75
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"follow.js","sourceRoot":"","sources":["../../../src/rules/test-names/follow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAiB,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAiC,MAAM,UAAU,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAEvD,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;AAErE,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;AAEnF,MAAM,IAAI,GAAG,kBAAkB,CAAC;AAEhC,MAAM,OAAO,GACZ,
|
|
1
|
+
{"version":3,"file":"follow.js","sourceRoot":"","sources":["../../../src/rules/test-names/follow.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/quality",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"types": "./dist/*.d.ts",
|
|
29
29
|
"default": "./dist/*.js"
|
|
30
30
|
},
|
|
31
|
-
"./biome": "./biome/preset.json",
|
|
31
|
+
"./biome/preset.json": "./biome/preset.json",
|
|
32
32
|
"./tsconfig/base.json": "./tsconfig/base.json",
|
|
33
33
|
"./tsconfig/package.json": "./tsconfig/package.json",
|
|
34
34
|
"./package.json": "./package.json",
|
|
@@ -7,7 +7,7 @@ import type { Environment } from "vitest/runtime";
|
|
|
7
7
|
// Resolve the DOM plugin from the consumer; depending on test-story would cycle through its Quality tooling.
|
|
8
8
|
const consumer = createRequire(join(process.cwd(), "package.json"));
|
|
9
9
|
const { default: environment }: { readonly default: Environment } = await import(
|
|
10
|
-
pathToFileURL(consumer.resolve("@shivaedev/test-story/domEnvironment.config.ts")).href
|
|
10
|
+
pathToFileURL(consumer.resolve("@shivaedev/test-story/server/domEnvironment.config.ts")).href
|
|
11
11
|
);
|
|
12
12
|
|
|
13
13
|
export default environment;
|
|
@@ -8,7 +8,7 @@ export interface Reached {
|
|
|
8
8
|
readonly target: Endpoint;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
interface Trail {
|
|
11
|
+
export interface Trail {
|
|
12
12
|
readonly line: number;
|
|
13
13
|
readonly path: readonly string[];
|
|
14
14
|
}
|
|
@@ -49,8 +49,13 @@ function follow(walk: Walk, before: Trail, edge: ImportEdge): void {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface Walked {
|
|
53
|
+
readonly reached: readonly Reached[];
|
|
54
|
+
readonly visited: ReadonlyMap<string, Trail>;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
// A breadth-first walk that stops at each forbidden endpoint, so a finding names the shortest path to the first forbidden step.
|
|
53
|
-
export function
|
|
58
|
+
export function walkFrom(start: string, edgesFrom: ReadonlyMap<string, readonly ImportEdge[]>, forbidden: Matcher): Walked {
|
|
54
59
|
const walk: Walk = { forbidden, queue: [start], reached: new Map(), trails: new Map([[start, { line: 0, path: [start] }]]) };
|
|
55
60
|
for (const node of walk.queue) {
|
|
56
61
|
const before = walk.trails.get(node) ?? { line: 0, path: [node] };
|
|
@@ -58,5 +63,9 @@ export function reachedFrom(start: string, edgesFrom: ReadonlyMap<string, readon
|
|
|
58
63
|
follow(walk, before, edge);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
|
-
return [...walk.reached.values()];
|
|
66
|
+
return { reached: [...walk.reached.values()], visited: walk.trails };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function reachedFrom(start: string, edgesFrom: ReadonlyMap<string, readonly ImportEdge[]>, forbidden: Matcher): readonly Reached[] {
|
|
70
|
+
return walkFrom(start, edgesFrom, forbidden).reached;
|
|
62
71
|
}
|
package/src/naming/testName.ts
CHANGED
package/src/rules/biome.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { biomeReport, type Diagnostic, FAILING } from "#biome/report.ts";
|
|
|
2
2
|
import { defineRule, type Finding, type Rule, type RuleInputs } from "#rule.ts";
|
|
3
3
|
import { itemsOf, member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
|
|
4
4
|
|
|
5
|
-
export const PRESET = "@shivaedev/quality/biome";
|
|
5
|
+
export const PRESET = "@shivaedev/quality/biome/preset.json";
|
|
6
6
|
|
|
7
7
|
const ROOT_CONFIGS: readonly string[] = ["biome.json", "biome.jsonc"];
|
|
8
8
|
|
package/src/rules/builtIn.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { noInline } from "#rules/suppressions/noInline.ts";
|
|
|
34
34
|
import { testsColocated } from "#rules/test-names/colocated.ts";
|
|
35
35
|
import { testsFollow } from "#rules/test-names/follow.ts";
|
|
36
36
|
import { biome } from "./biome.ts";
|
|
37
|
+
import { manifestsExportsShape } from "./manifestsExportsShape.ts";
|
|
37
38
|
import { manifestsSorted } from "./manifestsSorted.ts";
|
|
38
39
|
import { manifestsSourceFirst } from "./manifestsSourceFirst.ts";
|
|
39
40
|
import { maxDepth } from "./maxDepth.ts";
|
|
@@ -66,6 +67,7 @@ export const builtInRules = [
|
|
|
66
67
|
noTestCode,
|
|
67
68
|
manifestsSorted,
|
|
68
69
|
manifestsSourceFirst,
|
|
70
|
+
manifestsExportsShape,
|
|
69
71
|
namedAfterExport,
|
|
70
72
|
folderNames,
|
|
71
73
|
distinctCase,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { workspacePackages } from "#imports/workspace.ts";
|
|
3
|
+
import { defineRule, type Finding } from "#rule.ts";
|
|
4
|
+
|
|
5
|
+
const PATTERN = "./*.ts";
|
|
6
|
+
|
|
7
|
+
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
8
|
+
|
|
9
|
+
function isMap(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function standardPattern(key: string, value: unknown): boolean {
|
|
14
|
+
if (!(key.startsWith("./") && key.endsWith(".ts")) || key.split("*").length !== 2 || !isMap(value)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const subpath = key.slice("./".length);
|
|
18
|
+
const stem = subpath.slice(0, -".ts".length);
|
|
19
|
+
const conditions = { default: `./dist/${stem}.js`, source: `./src/${subpath}`, types: `./dist/${stem}.d.ts` };
|
|
20
|
+
return (
|
|
21
|
+
Object.keys(value).length === Object.keys(conditions).length
|
|
22
|
+
&& Object.entries(conditions).every(([condition, target]) => value[condition] === target)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exactAsset(key: string, value: unknown): boolean {
|
|
27
|
+
const extension = posix.extname(key);
|
|
28
|
+
return key.startsWith("./") && !key.includes("*") && value === key && extension !== "" && !CODE_EXTENSIONS.has(extension);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function finding(file: string, subject: string, reason: string): Finding {
|
|
32
|
+
return {
|
|
33
|
+
file,
|
|
34
|
+
message: `${reason} Import the module at its file path through the single "${PATTERN}" wildcard (for example, "./server/better-auth/mount.ts" for "./src/server/better-auth/mount.ts"). Map it to source "./src/*.ts", types "./dist/*.d.ts" and default "./dist/*.js"; remove aliases and per-file module exports. Non-code assets must use their exact file paths.`,
|
|
35
|
+
subject,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findingsIn(file: string, exports: unknown): readonly Finding[] {
|
|
40
|
+
if (exports === undefined) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
if (!isMap(exports)) {
|
|
44
|
+
return [finding(file, "exports", "The exports field must be a subpath map.")];
|
|
45
|
+
}
|
|
46
|
+
const entries = Object.entries(exports);
|
|
47
|
+
const patterns = entries.filter(([key, value]) => key.includes("*") && value !== null);
|
|
48
|
+
const count =
|
|
49
|
+
patterns.length === 1 ? [] : [finding(file, "exports", `The exports field needs exactly one non-null wildcard; found ${patterns.length}.`)];
|
|
50
|
+
return [
|
|
51
|
+
...count,
|
|
52
|
+
...entries.flatMap(([key, value]) =>
|
|
53
|
+
value === null || key === "." || exactAsset(key, value) || standardPattern(key, value)
|
|
54
|
+
? []
|
|
55
|
+
: [finding(file, key, `Export "${key}" does not preserve the module's file path.`)],
|
|
56
|
+
),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const manifestsExportsShape = defineRule({
|
|
61
|
+
check: async (inputs) => {
|
|
62
|
+
const packages = await workspacePackages(inputs);
|
|
63
|
+
const files = ["package.json", ...packages.map((owner) => posix.join(owner.directory, "package.json"))];
|
|
64
|
+
const findings = await Promise.all(
|
|
65
|
+
files.map(async (file) => {
|
|
66
|
+
const text = await inputs.readText(file);
|
|
67
|
+
if (file === "package.json" && text === undefined) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const manifest: unknown = JSON.parse(text ?? "");
|
|
71
|
+
return findingsIn(file, isMap(manifest) ? manifest.exports : undefined);
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
return findings.flat();
|
|
75
|
+
},
|
|
76
|
+
description: "Workspace packages export modules through one file-preserving wildcard, with optional root, exact non-code assets and null blocks.",
|
|
77
|
+
id: "manifests/exports-shape",
|
|
78
|
+
registrable: false,
|
|
79
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Endpoint } from "#imports/resolve.ts";
|
|
2
|
+
|
|
3
|
+
const ENGINES: ReadonlyArray<readonly [string, readonly string[]]> = [
|
|
4
|
+
["the file system", ["node:fs"]],
|
|
5
|
+
[
|
|
6
|
+
"the network",
|
|
7
|
+
[
|
|
8
|
+
"node:dgram",
|
|
9
|
+
"node:dns",
|
|
10
|
+
"node:http",
|
|
11
|
+
"node:http2",
|
|
12
|
+
"node:https",
|
|
13
|
+
"node:net",
|
|
14
|
+
"node:tls",
|
|
15
|
+
"effect/http",
|
|
16
|
+
"effect/socket",
|
|
17
|
+
"@effect/platform-browser",
|
|
18
|
+
"@effect/platform-bun",
|
|
19
|
+
"@effect/platform-node",
|
|
20
|
+
"@effect/platform-node-shared",
|
|
21
|
+
"@trpc/client",
|
|
22
|
+
"@trpc/server",
|
|
23
|
+
"express",
|
|
24
|
+
"fastify",
|
|
25
|
+
"hono",
|
|
26
|
+
"undici",
|
|
27
|
+
"ws",
|
|
28
|
+
],
|
|
29
|
+
],
|
|
30
|
+
["a process", ["node:child_process", "node:cluster", "node:process", "node:readline", "effect/process", "claude-code"]],
|
|
31
|
+
["a worker", ["node:worker_threads", "effect/workers"]],
|
|
32
|
+
[
|
|
33
|
+
"a database",
|
|
34
|
+
[
|
|
35
|
+
"node:sqlite",
|
|
36
|
+
"effect/persistence",
|
|
37
|
+
"effect/sql",
|
|
38
|
+
"@effect/sql",
|
|
39
|
+
"@effect/sql-pg",
|
|
40
|
+
"@effect/sql-sqlite-node",
|
|
41
|
+
"@electric-sql/pglite",
|
|
42
|
+
"@prisma/client",
|
|
43
|
+
"@prisma-next/",
|
|
44
|
+
"better-auth",
|
|
45
|
+
"better-sqlite3",
|
|
46
|
+
"mysql2",
|
|
47
|
+
"pg",
|
|
48
|
+
"pg-boss",
|
|
49
|
+
"postgres",
|
|
50
|
+
],
|
|
51
|
+
],
|
|
52
|
+
["the DOM", ["@happy-dom/", "@playwright/test", "@testing-library/", "happy-dom", "jsdom", "playwright", "react-dom"]],
|
|
53
|
+
["a story engine", ["@shivaedev/effect-test-sql", "@shivaedev/local-postgres", "@shivaedev/test-story"]],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function names(prefix: string, specifier: string): boolean {
|
|
57
|
+
return prefix.endsWith("/") ? specifier.startsWith(prefix) : specifier === prefix || specifier.startsWith(`${prefix}/`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function engineOf(endpoint: Endpoint): string | undefined {
|
|
61
|
+
if (endpoint.kind === "file") {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return ENGINES.find(([, prefixes]) => prefixes.some((prefix) => names(prefix, endpoint.specifier) || names(prefix, endpoint.package)))?.[0];
|
|
65
|
+
}
|
|
@@ -19,7 +19,7 @@ function moduleOrigin(from: string | undefined): Origin {
|
|
|
19
19
|
if (from === "vitest" || from === "@effect/vitest") {
|
|
20
20
|
return "vitest";
|
|
21
21
|
}
|
|
22
|
-
return from === "@shivaedev/test-story" || from === "@shivaedev/test-story/storyKit.ts" ? "stories" : undefined;
|
|
22
|
+
return from === "@shivaedev/test-story" || from === "@shivaedev/test-story/server/storyKit.ts" ? "stories" : undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function imported(name: string, from: string | undefined): Origin {
|
|
@@ -97,7 +97,7 @@ export function storyOrigins(inputs: RuleInputs, graph: ImportGraph) {
|
|
|
97
97
|
return receiver;
|
|
98
98
|
}
|
|
99
99
|
const from = receiver === "vitest" ? "vitest" : undefined;
|
|
100
|
-
return imported(name, receiver === "stories" ? "@shivaedev/test-story/storyKit.ts" : from);
|
|
100
|
+
return imported(name, receiver === "stories" ? "@shivaedev/test-story/server/storyKit.ts" : from);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function identifier(node: ts.Identifier): Origin {
|
|
@@ -5,6 +5,7 @@ import { testName } from "#naming/testName.ts";
|
|
|
5
5
|
import { defineRule } from "#rule.ts";
|
|
6
6
|
import { storyOrigins } from "./origins.ts";
|
|
7
7
|
import { storyPrerequisites } from "./prerequisites.ts";
|
|
8
|
+
import { unitTests } from "./unitTests.ts";
|
|
8
9
|
|
|
9
10
|
function namesTest(node: ts.Expression): boolean {
|
|
10
11
|
if (ts.isIdentifier(node)) {
|
|
@@ -57,8 +58,12 @@ export const storySpecs = defineRule({
|
|
|
57
58
|
const graph = await importGraph(inputs);
|
|
58
59
|
const exempt = await storyPrerequisites(inputs, graph.packages);
|
|
59
60
|
const origins = storyOrigins(inputs, graph);
|
|
61
|
+
const unit = unitTests(graph, inputs.sources);
|
|
60
62
|
return inputs.sources.flatMap((file) => {
|
|
61
63
|
const name = testName(file.path);
|
|
64
|
+
if (name?.modifiers.includes("unit") === true) {
|
|
65
|
+
return unit(file.path);
|
|
66
|
+
}
|
|
62
67
|
const owner = packageOf(graph.packages, file.path);
|
|
63
68
|
if (name === undefined || name.modifiers.includes("typecheck") || exempt.has(owner?.directory ?? "")) {
|
|
64
69
|
return [];
|
|
@@ -81,13 +86,12 @@ export const storySpecs = defineRule({
|
|
|
81
86
|
{
|
|
82
87
|
file: file.path,
|
|
83
88
|
line: source.getLineAndCharacterOfPosition(plain.getStart(source)).line + 1,
|
|
84
|
-
message:
|
|
85
|
-
"Declares tests without a test-story story kit. Build the package's kit with storyKit from @shivaedev/test-story/storyKit.ts in src/test-support, and declare every test through the kit's it. See https://github.com/ShivaeDev/platform/tree/main/packages/quality#story-tests.",
|
|
89
|
+
message: `Declares tests without a test-story story kit. Drive the engine through the package's story kit: build it with storyKit from @shivaedev/test-story/server/storyKit.ts in src/test-support, and declare every test through the kit's it. If the code under test is pure, move the test to ${name.stem}.unit.${name.kind}${name.extension}, which reaches no engine and mocks nothing. See https://github.com/ShivaeDev/platform/tree/main/packages/quality#story-tests.`,
|
|
86
90
|
},
|
|
87
91
|
];
|
|
88
92
|
});
|
|
89
93
|
},
|
|
90
94
|
description:
|
|
91
|
-
"Every package tests its real engine through its own test-story kit. Typecheck tests and test-story's workspace prerequisites
|
|
95
|
+
"Every package tests its real engine through its own test-story kit, and a plain test lives only in a .unit file that reaches no engine and mocks nothing. Typecheck tests and test-story's workspace prerequisites need no kit.",
|
|
92
96
|
id: "tests/story-specs",
|
|
93
97
|
});
|