@elench/testkit 0.1.46 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/lib/cli/args.mjs +25 -4
- package/lib/cli/args.test.mjs +32 -0
- package/lib/cli/db.mjs +115 -0
- package/lib/cli/index.mjs +23 -1
- package/lib/cli/known-failures.mjs +164 -0
- package/lib/config/index.mjs +155 -28
- package/lib/database/fingerprint.mjs +9 -5
- package/lib/database/fingerprint.test.mjs +10 -4
- package/lib/database/index.mjs +34 -11
- package/lib/database/template-steps.mjs +232 -0
- package/lib/runner/runtime-contexts.mjs +2 -41
- package/lib/runner/template.mjs +30 -24
- package/lib/runner/template.test.mjs +1 -2
- package/lib/setup/index.d.ts +37 -7
- package/lib/setup/index.mjs +21 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,13 @@ npx @elench/testkit --type int --write-status
|
|
|
47
47
|
# Lifecycle
|
|
48
48
|
npx @elench/testkit status
|
|
49
49
|
npx @elench/testkit destroy
|
|
50
|
+
|
|
51
|
+
# Known-failures tooling
|
|
52
|
+
npx @elench/testkit known-failures validate --issue-mode error
|
|
53
|
+
npx @elench/testkit known-failures render --output KNOWN_FAILURES.md
|
|
54
|
+
|
|
55
|
+
# Capture a template DB schema snapshot
|
|
56
|
+
npx @elench/testkit db snapshot capture --service api --output scripts/testkit/schema-baseline.sql
|
|
50
57
|
```
|
|
51
58
|
|
|
52
59
|
## Setup
|
|
@@ -55,11 +62,13 @@ Create `testkit.setup.ts` at repo root:
|
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
64
|
import {
|
|
65
|
+
commandStep,
|
|
58
66
|
defineTestkitSetup,
|
|
59
|
-
lifecycle,
|
|
60
67
|
localDatabase,
|
|
68
|
+
moduleStep,
|
|
61
69
|
nextService,
|
|
62
70
|
service,
|
|
71
|
+
sqlFileStep,
|
|
63
72
|
tsxService,
|
|
64
73
|
} from "@elench/testkit/setup";
|
|
65
74
|
|
|
@@ -85,9 +94,13 @@ export default defineTestkitSetup({
|
|
|
85
94
|
readyPath: "/health",
|
|
86
95
|
}),
|
|
87
96
|
envFiles: [".env.testkit"],
|
|
88
|
-
database: localDatabase(
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
database: localDatabase({
|
|
98
|
+
template: {
|
|
99
|
+
inputs: ["db/schema.sql", "scripts/seed.ts"],
|
|
100
|
+
migrate: [sqlFileStep("db/schema.sql")],
|
|
101
|
+
seed: [commandStep("npm run db:seed")],
|
|
102
|
+
verify: [moduleStep("src/testkit/verify-seed.ts#verifySeed")],
|
|
103
|
+
},
|
|
91
104
|
}),
|
|
92
105
|
runtime: {
|
|
93
106
|
instances: 1,
|
|
@@ -145,9 +158,9 @@ for:
|
|
|
145
158
|
- local runtime instance counts
|
|
146
159
|
- per-runtime concurrent task caps
|
|
147
160
|
- local DB binding configuration
|
|
161
|
+
- template database migrate / seed / verify stages
|
|
162
|
+
- template schema snapshot capture
|
|
148
163
|
- explicit per-file or per-suite locks
|
|
149
|
-
- migrate / seed commands
|
|
150
|
-
- test-local migrate / seed overrides
|
|
151
164
|
- named HTTP suite profiles
|
|
152
165
|
- known-failure annotation merge for enriched status/run artifacts
|
|
153
166
|
- optional GitHub-backed known-failure issue validation
|
|
@@ -190,6 +203,11 @@ Reproduction warnings are execution-aware:
|
|
|
190
203
|
- `passed` means the matched test executed and did not reproduce
|
|
191
204
|
- `skipped` and `not_run` do not count as reproduction evidence
|
|
192
205
|
|
|
206
|
+
Known-failure operations are also available as first-class CLI commands:
|
|
207
|
+
|
|
208
|
+
- `testkit known-failures validate`
|
|
209
|
+
- `testkit known-failures render`
|
|
210
|
+
|
|
193
211
|
## Authoring
|
|
194
212
|
|
|
195
213
|
HTTP suites:
|
package/lib/cli/args.mjs
CHANGED
|
@@ -7,16 +7,37 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export const POSITIONAL_TYPES = new Set(["int", "e2e", "dal", "load", "pw", "all"]);
|
|
9
9
|
export const LIFECYCLE = new Set(["status", "destroy", "cleanup"]);
|
|
10
|
+
export const KNOWN_FAILURES_ACTIONS = new Set(["render", "validate"]);
|
|
10
11
|
export const RESERVED = new Set([...POSITIONAL_TYPES, ...LIFECYCLE]);
|
|
11
12
|
|
|
12
13
|
export function resolveCliSelection({ first, second, third }) {
|
|
14
|
+
let lifecycle = null;
|
|
15
|
+
let positionalType = null;
|
|
16
|
+
let knownFailuresAction = null;
|
|
17
|
+
let dbAction = null;
|
|
18
|
+
|
|
19
|
+
if (first === "known-failures") {
|
|
20
|
+
if (!second || !KNOWN_FAILURES_ACTIONS.has(second) || third) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'Unknown known-failures command. Expected "known-failures render" or "known-failures validate".'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
knownFailuresAction = second;
|
|
26
|
+
return { lifecycle, positionalType, knownFailuresAction, dbAction };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (first === "db") {
|
|
30
|
+
if (second === "snapshot" && third === "capture") {
|
|
31
|
+
dbAction = "snapshot-capture";
|
|
32
|
+
return { lifecycle, positionalType, knownFailuresAction, dbAction };
|
|
33
|
+
}
|
|
34
|
+
throw new Error('Unknown db command. Expected "db snapshot capture".');
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
if (second || third) {
|
|
14
38
|
throw new Error(`Unexpected extra positional arguments. Use --service and --type instead.`);
|
|
15
39
|
}
|
|
16
40
|
|
|
17
|
-
let lifecycle = null;
|
|
18
|
-
let positionalType = null;
|
|
19
|
-
|
|
20
41
|
if (first && LIFECYCLE.has(first)) {
|
|
21
42
|
lifecycle = first;
|
|
22
43
|
} else if (first && POSITIONAL_TYPES.has(first)) {
|
|
@@ -28,7 +49,7 @@ export function resolveCliSelection({ first, second, third }) {
|
|
|
28
49
|
);
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
return { lifecycle, positionalType };
|
|
52
|
+
return { lifecycle, positionalType, knownFailuresAction, dbAction };
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
export function parseTypeOption(values, positionalType = null) {
|
package/lib/cli/args.test.mjs
CHANGED
|
@@ -18,6 +18,8 @@ describe("cli-args", () => {
|
|
|
18
18
|
third: null,
|
|
19
19
|
})
|
|
20
20
|
).toEqual({
|
|
21
|
+
dbAction: null,
|
|
22
|
+
knownFailuresAction: null,
|
|
21
23
|
lifecycle: null,
|
|
22
24
|
positionalType: "int",
|
|
23
25
|
});
|
|
@@ -31,11 +33,41 @@ describe("cli-args", () => {
|
|
|
31
33
|
third: null,
|
|
32
34
|
})
|
|
33
35
|
).toEqual({
|
|
36
|
+
dbAction: null,
|
|
37
|
+
knownFailuresAction: null,
|
|
34
38
|
lifecycle: "status",
|
|
35
39
|
positionalType: null,
|
|
36
40
|
});
|
|
37
41
|
});
|
|
38
42
|
|
|
43
|
+
it("resolves known-failures and db subcommands", () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveCliSelection({
|
|
46
|
+
first: "known-failures",
|
|
47
|
+
second: "render",
|
|
48
|
+
third: null,
|
|
49
|
+
})
|
|
50
|
+
).toEqual({
|
|
51
|
+
dbAction: null,
|
|
52
|
+
knownFailuresAction: "render",
|
|
53
|
+
lifecycle: null,
|
|
54
|
+
positionalType: null,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
resolveCliSelection({
|
|
59
|
+
first: "db",
|
|
60
|
+
second: "snapshot",
|
|
61
|
+
third: "capture",
|
|
62
|
+
})
|
|
63
|
+
).toEqual({
|
|
64
|
+
dbAction: "snapshot-capture",
|
|
65
|
+
knownFailuresAction: null,
|
|
66
|
+
lifecycle: null,
|
|
67
|
+
positionalType: null,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
39
71
|
it("rejects unknown positional arguments", () => {
|
|
40
72
|
expect(() =>
|
|
41
73
|
resolveCliSelection({
|
package/lib/cli/db.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { loadConfigs, resolveProductDir } from "../config/index.mjs";
|
|
5
|
+
import { captureDatabaseTemplateSnapshot, prepareDatabaseRuntime } from "../database/index.mjs";
|
|
6
|
+
import { resolveRuntimeInstanceConfigs } from "../runner/template.mjs";
|
|
7
|
+
|
|
8
|
+
export async function runDatabaseSnapshotCaptureCommand(options = {}) {
|
|
9
|
+
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
10
|
+
const configs = await loadConfigs({ dir: productDir });
|
|
11
|
+
const target = resolveTargetConfig(configs, options.service);
|
|
12
|
+
const outputPath = normalizeOptionalString(options.output);
|
|
13
|
+
if (!outputPath) {
|
|
14
|
+
throw new Error("Snapshot capture requires --output");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const selectedConfigs = collectRequiredConfigs(configs, target.name);
|
|
18
|
+
const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-snapshot-"));
|
|
19
|
+
const runtimeDir = path.join(runtimeRoot, "runtime-1");
|
|
20
|
+
const resolvedConfigs = resolveRuntimeInstanceConfigs(selectedConfigs, "runtime-1", runtimeDir, {
|
|
21
|
+
graphDirName: `snapshot-${target.name}`,
|
|
22
|
+
portNamespaceIndex: 0,
|
|
23
|
+
portNamespaceStride: 1,
|
|
24
|
+
});
|
|
25
|
+
const resolvedTarget = resolvedConfigs.find((config) => config.name === target.name);
|
|
26
|
+
if (!resolvedTarget) {
|
|
27
|
+
throw new Error(`Resolved runtime config missing target service "${target.name}"`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const absoluteOutputPath = path.resolve(productDir, outputPath);
|
|
31
|
+
try {
|
|
32
|
+
for (const config of topologicallySortConfigs(resolvedConfigs)) {
|
|
33
|
+
if (config.name === resolvedTarget.name) continue;
|
|
34
|
+
if (config.testkit.database?.provider === "local") {
|
|
35
|
+
await prepareDatabaseRuntime(config);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await captureDatabaseTemplateSnapshot(resolvedTarget, absoluteOutputPath);
|
|
39
|
+
console.log(`Wrote ${path.relative(productDir, absoluteOutputPath)}`);
|
|
40
|
+
} finally {
|
|
41
|
+
fs.rmSync(runtimeRoot, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveTargetConfig(configs, serviceName) {
|
|
46
|
+
if (serviceName) {
|
|
47
|
+
const match = configs.find((config) => config.name === serviceName);
|
|
48
|
+
if (!match) {
|
|
49
|
+
const available = configs.map((config) => config.name).join(", ");
|
|
50
|
+
throw new Error(`Service "${serviceName}" not found. Available: ${available}`);
|
|
51
|
+
}
|
|
52
|
+
return match;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (configs.length === 1) return configs[0];
|
|
56
|
+
const withLocalDb = configs.filter((config) => config.testkit.database?.provider === "local");
|
|
57
|
+
if (withLocalDb.length === 1) return withLocalDb[0];
|
|
58
|
+
|
|
59
|
+
const available = configs.map((config) => config.name).join(", ");
|
|
60
|
+
throw new Error(`Multiple services available. Pass --service. Available: ${available}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeOptionalString(value) {
|
|
64
|
+
if (typeof value !== "string") return null;
|
|
65
|
+
const normalized = value.trim();
|
|
66
|
+
return normalized.length > 0 ? normalized : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectRequiredConfigs(configs, targetName) {
|
|
70
|
+
const byName = new Map(configs.map((config) => [config.name, config]));
|
|
71
|
+
const required = new Set();
|
|
72
|
+
|
|
73
|
+
const visit = (name) => {
|
|
74
|
+
if (required.has(name)) return;
|
|
75
|
+
const config = byName.get(name);
|
|
76
|
+
if (!config) {
|
|
77
|
+
throw new Error(`Missing config for dependency "${name}"`);
|
|
78
|
+
}
|
|
79
|
+
required.add(name);
|
|
80
|
+
for (const depName of config.testkit.dependsOn || []) {
|
|
81
|
+
visit(depName);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
visit(targetName);
|
|
86
|
+
return configs.filter((config) => required.has(config.name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function topologicallySortConfigs(configs) {
|
|
90
|
+
const byName = new Map(configs.map((config) => [config.name, config]));
|
|
91
|
+
const visited = new Set();
|
|
92
|
+
const visiting = new Set();
|
|
93
|
+
const ordered = [];
|
|
94
|
+
|
|
95
|
+
const visit = (config) => {
|
|
96
|
+
if (visited.has(config.name)) return;
|
|
97
|
+
if (visiting.has(config.name)) {
|
|
98
|
+
throw new Error(`Dependency cycle while resolving snapshot capture for "${config.name}"`);
|
|
99
|
+
}
|
|
100
|
+
visiting.add(config.name);
|
|
101
|
+
for (const depName of config.testkit.dependsOn || []) {
|
|
102
|
+
const dependency = byName.get(depName);
|
|
103
|
+
if (dependency) visit(dependency);
|
|
104
|
+
}
|
|
105
|
+
visiting.delete(config.name);
|
|
106
|
+
visited.add(config.name);
|
|
107
|
+
ordered.push(config);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const config of configs) {
|
|
111
|
+
visit(config);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return ordered;
|
|
115
|
+
}
|
package/lib/cli/index.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
2
|
import { loadConfigs } from "../config/index.mjs";
|
|
3
|
+
import {
|
|
4
|
+
runKnownFailuresRenderCommand,
|
|
5
|
+
runKnownFailuresValidateCommand,
|
|
6
|
+
} from "./known-failures.mjs";
|
|
7
|
+
import { runDatabaseSnapshotCaptureCommand } from "./db.mjs";
|
|
3
8
|
import {
|
|
4
9
|
parseFileTimeoutOption,
|
|
5
10
|
parseShardOption,
|
|
@@ -28,16 +33,33 @@ export async function run(argv = process.argv) {
|
|
|
28
33
|
.option("--shard <i/n>", "Run only shard i of n at suite granularity")
|
|
29
34
|
.option("--write-status", "Write a deterministic testkit.status.json snapshot")
|
|
30
35
|
.option("--allow-partial-status", "Allow --write-status for filtered runs")
|
|
36
|
+
.option("--input <path>", "Known failures JSON path (repo-relative)")
|
|
37
|
+
.option("--output <path>", "Output path for render/snapshot commands")
|
|
38
|
+
.option("--status <path>", "Status artifact path for validation commands")
|
|
39
|
+
.option("--issue-mode <mode>", "Issue validation mode override: off, warn, error")
|
|
31
40
|
.option(
|
|
32
41
|
"--ignore-skip-rules",
|
|
33
42
|
"Run files even if testkit.setup.ts marks them skipped"
|
|
34
43
|
)
|
|
35
44
|
.action(async (first, second, third, options) => {
|
|
36
|
-
const { lifecycle, positionalType } = resolveCliSelection({
|
|
45
|
+
const { lifecycle, positionalType, knownFailuresAction, dbAction } = resolveCliSelection({
|
|
37
46
|
first,
|
|
38
47
|
second,
|
|
39
48
|
third,
|
|
40
49
|
});
|
|
50
|
+
if (knownFailuresAction === "render") {
|
|
51
|
+
await runKnownFailuresRenderCommand(options);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (knownFailuresAction === "validate") {
|
|
55
|
+
await runKnownFailuresValidateCommand(options);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (dbAction === "snapshot-capture") {
|
|
59
|
+
await runDatabaseSnapshotCaptureCommand(options);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
const allConfigs = await loadConfigs({ dir: options.dir });
|
|
42
64
|
const configs = options.service
|
|
43
65
|
? allConfigs.filter((config) => config.name === options.service)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveProductDir } from "../config/index.mjs";
|
|
4
|
+
import { loadTestkitSetup } from "../config/setup-loader.mjs";
|
|
5
|
+
import {
|
|
6
|
+
buildKnownFailureIssueValidationSummaryLines,
|
|
7
|
+
normalizeKnownFailureIssueValidationConfig,
|
|
8
|
+
shouldFailKnownFailureIssueValidation,
|
|
9
|
+
validateKnownFailureIssues,
|
|
10
|
+
} from "../known-failures/github.mjs";
|
|
11
|
+
import {
|
|
12
|
+
loadKnownFailuresDocument,
|
|
13
|
+
renderKnownFailuresMarkdown,
|
|
14
|
+
validateKnownFailuresDocument,
|
|
15
|
+
} from "../known-failures/index.mjs";
|
|
16
|
+
import { collectGitMetadata } from "../runner/metadata.mjs";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_ISSUE_VALIDATION = {
|
|
19
|
+
provider: "github",
|
|
20
|
+
mode: "error",
|
|
21
|
+
cacheTtlSeconds: 900,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function runKnownFailuresRenderCommand(options = {}) {
|
|
25
|
+
const context = await resolveKnownFailuresContext(options);
|
|
26
|
+
const document = loadKnownFailuresDocument(context.knownFailuresPath, context.knownFailuresRelativePath);
|
|
27
|
+
const markdown = renderKnownFailuresMarkdown(document);
|
|
28
|
+
|
|
29
|
+
if (options.output) {
|
|
30
|
+
const outputPath = path.resolve(context.productDir, options.output);
|
|
31
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(outputPath, markdown);
|
|
33
|
+
console.log(`Wrote ${path.relative(context.productDir, outputPath)}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process.stdout.write(markdown);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runKnownFailuresValidateCommand(options = {}) {
|
|
41
|
+
const context = await resolveKnownFailuresContext(options);
|
|
42
|
+
const document = loadKnownFailuresDocument(context.knownFailuresPath, context.knownFailuresRelativePath);
|
|
43
|
+
const statusArtifact = context.statusArtifactPath
|
|
44
|
+
? readJsonIfExists(context.statusArtifactPath)
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
const documentValidation = validateKnownFailuresDocument(document, {
|
|
48
|
+
productDir: context.productDir,
|
|
49
|
+
statusArtifact,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const issueValidation = await validateKnownFailureIssues({
|
|
53
|
+
productDir: context.productDir,
|
|
54
|
+
document,
|
|
55
|
+
statusArtifact,
|
|
56
|
+
config: context.issueValidationConfig,
|
|
57
|
+
gitMetadata: collectGitMetadata(context.productDir),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const issueErrors = collectIssueMessages(issueValidation, "error");
|
|
61
|
+
const issueWarnings = collectIssueMessages(issueValidation, "warning");
|
|
62
|
+
const hasErrors =
|
|
63
|
+
documentValidation.errors.length > 0 ||
|
|
64
|
+
shouldFailKnownFailureIssueValidation(issueValidation) ||
|
|
65
|
+
issueErrors.length > 0;
|
|
66
|
+
|
|
67
|
+
if (hasErrors) {
|
|
68
|
+
console.error("Known failures validation failed:");
|
|
69
|
+
for (const error of documentValidation.errors) {
|
|
70
|
+
console.error(`- ${error}`);
|
|
71
|
+
}
|
|
72
|
+
for (const error of issueErrors) {
|
|
73
|
+
console.error(`- ${error}`);
|
|
74
|
+
}
|
|
75
|
+
if (documentValidation.warnings.length > 0 || issueWarnings.length > 0) {
|
|
76
|
+
console.error("");
|
|
77
|
+
for (const warning of documentValidation.warnings) {
|
|
78
|
+
console.error(`! ${warning}`);
|
|
79
|
+
}
|
|
80
|
+
for (const warning of issueWarnings) {
|
|
81
|
+
console.error(`! ${warning}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const line of buildKnownFailureIssueValidationSummaryLines(issueValidation)) {
|
|
85
|
+
console.error(line);
|
|
86
|
+
}
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(
|
|
92
|
+
`Known failures valid: ${documentValidation.stats.entries} entries, ${documentValidation.stats.matches} matches`
|
|
93
|
+
);
|
|
94
|
+
if (documentValidation.stats.failedTests > 0) {
|
|
95
|
+
console.log(
|
|
96
|
+
`Status coverage: ${documentValidation.stats.triagedFailedTests}/${documentValidation.stats.failedTests} failed tests triaged`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
for (const line of buildKnownFailureIssueValidationSummaryLines(issueValidation)) {
|
|
100
|
+
console.log(line);
|
|
101
|
+
}
|
|
102
|
+
for (const warning of [...documentValidation.warnings, ...issueWarnings]) {
|
|
103
|
+
console.warn(`! ${warning}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function resolveKnownFailuresContext(options) {
|
|
108
|
+
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
109
|
+
const { setup } = await loadTestkitSetup(productDir);
|
|
110
|
+
const reporting = setup?.reporting || {};
|
|
111
|
+
|
|
112
|
+
const knownFailuresRelativePath = normalizeOptionalString(options.input)
|
|
113
|
+
|| normalizeOptionalString(reporting.knownFailuresFile);
|
|
114
|
+
if (!knownFailuresRelativePath) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Known failures file not configured. Set reporting.knownFailuresFile in testkit.setup.ts or pass --input."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const statusRelativePath = normalizeOptionalString(options.status) || "testkit.status.json";
|
|
121
|
+
const configuredIssueValidation = normalizeKnownFailureIssueValidationConfig(reporting.issueValidation)
|
|
122
|
+
|| DEFAULT_ISSUE_VALIDATION;
|
|
123
|
+
const issueMode = normalizeOptionalString(options.issueMode) || "error";
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
productDir,
|
|
127
|
+
knownFailuresRelativePath,
|
|
128
|
+
knownFailuresPath: path.resolve(productDir, knownFailuresRelativePath),
|
|
129
|
+
statusArtifactPath: statusRelativePath ? path.resolve(productDir, statusRelativePath) : null,
|
|
130
|
+
issueValidationConfig: {
|
|
131
|
+
...configuredIssueValidation,
|
|
132
|
+
mode: issueMode,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function collectIssueMessages(validation, severity) {
|
|
138
|
+
if (!validation) return [];
|
|
139
|
+
|
|
140
|
+
const messages = [
|
|
141
|
+
...validation.findings
|
|
142
|
+
.filter((finding) => finding.severity === severity)
|
|
143
|
+
.map((finding) => finding.message),
|
|
144
|
+
];
|
|
145
|
+
for (const entry of validation.entries) {
|
|
146
|
+
for (const finding of entry.findings) {
|
|
147
|
+
if (finding.severity === severity) {
|
|
148
|
+
messages.push(finding.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...new Set(messages)];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeOptionalString(value) {
|
|
156
|
+
if (typeof value !== "string") return null;
|
|
157
|
+
const normalized = value.trim();
|
|
158
|
+
return normalized.length > 0 ? normalized : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readJsonIfExists(filePath) {
|
|
162
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
163
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
164
|
+
}
|