@elench/testkit 0.1.80 → 0.1.82
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 +78 -56
- package/lib/cli/args.mjs +2 -14
- package/lib/cli/args.test.mjs +1 -17
- package/lib/cli/command-helpers.mjs +1 -20
- package/lib/cli/entrypoint.mjs +0 -4
- package/lib/cli/presentation/colors.mjs +1 -1
- package/lib/cli/presentation/failure-presentation.mjs +4 -4
- package/lib/cli/presentation/run-reporter.mjs +23 -9
- package/lib/cli/presentation/run-reporter.test.mjs +12 -6
- package/lib/cli/presentation/summary-box.test.mjs +4 -4
- package/lib/cli/viewer.mjs +18 -19
- package/lib/config/index.mjs +6 -6
- package/lib/config/runtime.mjs +8 -8
- package/lib/config-api/auth-fixtures.mjs +762 -0
- package/lib/config-api/index.d.ts +96 -112
- package/lib/config-api/index.mjs +22 -12
- package/lib/config-api/index.test.mjs +61 -222
- package/lib/index.d.ts +29 -9
- package/lib/package.test.mjs +4 -4
- package/lib/{known-failures → regressions}/github.mjs +36 -78
- package/lib/regressions/github.test.mjs +324 -0
- package/lib/regressions/index.d.ts +189 -0
- package/lib/{known-failures → regressions}/index.mjs +90 -93
- package/lib/{known-failures → regressions}/index.test.mjs +37 -48
- package/lib/runner/formatting.mjs +49 -34
- package/lib/runner/formatting.test.mjs +16 -15
- package/lib/runner/metadata.mjs +1 -1
- package/lib/runner/orchestrator.mjs +7 -9
- package/lib/runner/regressions.mjs +304 -0
- package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
- package/lib/runner/reporting.mjs +2 -2
- package/lib/runner/reporting.test.mjs +2 -2
- package/lib/runner/run-finalization.mjs +18 -30
- package/lib/runner/template-steps.mjs +2 -2
- package/lib/runtime/index.d.ts +50 -33
- package/lib/runtime/index.mjs +0 -1
- package/lib/runtime-src/k6/http-suite-runtime.js +147 -0
- package/lib/runtime-src/k6/http.js +80 -41
- package/lib/runtime-src/k6/scenario-suite.js +13 -110
- package/lib/runtime-src/k6/suite.js +13 -107
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +8 -8
- package/lib/cli/commands/known-failures/render.mjs +0 -19
- package/lib/cli/commands/known-failures/validate.mjs +0 -20
- package/lib/cli/known-failures.mjs +0 -164
- package/lib/config-api/profiles.mjs +0 -640
- package/lib/known-failures/github.test.mjs +0 -512
- package/lib/known-failures/index.d.ts +0 -192
- package/lib/runner/triage.mjs +0 -221
- /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
- /package/lib/{known-failures → regressions}/github-transport.mjs +0 -0
package/README.md
CHANGED
|
@@ -62,9 +62,8 @@ npx @elench/testkit artifacts __testkit__/health/health.int.testkit.ts
|
|
|
62
62
|
npx @elench/testkit logs __testkit__/health/health.int.testkit.ts
|
|
63
63
|
npx @elench/testkit watch
|
|
64
64
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
npx @elench/testkit known-failures render --output KNOWN_FAILURES.md
|
|
65
|
+
# Automatic regression intelligence
|
|
66
|
+
# Configure testkit.regressions.json and testkit classifies new vs known regressions automatically during runs
|
|
68
67
|
|
|
69
68
|
# Capture a template DB schema snapshot
|
|
70
69
|
npx @elench/testkit db snapshot capture --service api --output scripts/testkit/schema-baseline.sql
|
|
@@ -81,6 +80,29 @@ persisted under `.testkit/results/` and inspected on demand with `show`,
|
|
|
81
80
|
run counts, pass/fail/skip counts, average duration, and last observed status,
|
|
82
81
|
and those summaries are exposed in compact, verbose, and JSON discovery output.
|
|
83
82
|
|
|
83
|
+
## Automatic Regression Diagnosis
|
|
84
|
+
|
|
85
|
+
If `regressions.file` is configured, every run automatically classifies observed
|
|
86
|
+
results without any separate follow-up maintenance command.
|
|
87
|
+
|
|
88
|
+
`testkit` distinguishes four user-facing outcomes:
|
|
89
|
+
|
|
90
|
+
- `new regressions`
|
|
91
|
+
- `known regressions`
|
|
92
|
+
- `fixed known regressions`
|
|
93
|
+
- `catalog stale`
|
|
94
|
+
|
|
95
|
+
The default CLI keeps those signals lightweight:
|
|
96
|
+
|
|
97
|
+
- failed files print inline diagnosis immediately under the file line
|
|
98
|
+
- the final summary box reports aggregate regression counts only
|
|
99
|
+
- machine-readable artifacts gain per-file `diagnosis` plus top-level
|
|
100
|
+
`regressions.summary`, `regressions.catalog`, and prepared `regressions.drafts`
|
|
101
|
+
|
|
102
|
+
`catalog stale` is repo hygiene, not a test failure. It means the regression
|
|
103
|
+
catalog or linked issue tracker metadata needs attention, for example because a
|
|
104
|
+
linked issue is closed but the regression still reproduces.
|
|
105
|
+
|
|
84
106
|
## Tooling Adapters
|
|
85
107
|
|
|
86
108
|
`testkit` also ships tool-specific config helpers so consumer repos do not need
|
|
@@ -159,9 +181,9 @@ export default defineConfig({
|
|
|
159
181
|
workers: 8,
|
|
160
182
|
fileTimeoutSeconds: 60,
|
|
161
183
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
regressions: {
|
|
185
|
+
file: "testkit.regressions.json",
|
|
186
|
+
sync: {
|
|
165
187
|
provider: "github",
|
|
166
188
|
mode: "warn",
|
|
167
189
|
cacheTtlSeconds: 900,
|
|
@@ -240,8 +262,8 @@ for:
|
|
|
240
262
|
- template schema snapshot capture
|
|
241
263
|
- explicit per-file or per-suite locks
|
|
242
264
|
- named HTTP suite profiles
|
|
243
|
-
-
|
|
244
|
-
- optional GitHub-backed
|
|
265
|
+
- automatic regression classification for new vs known failures
|
|
266
|
+
- optional GitHub-backed regression issue sync
|
|
245
267
|
- repo-declared suite/file skip policies with explicit reasons
|
|
246
268
|
- telemetry upload configuration
|
|
247
269
|
|
|
@@ -303,47 +325,40 @@ services: {
|
|
|
303
325
|
}
|
|
304
326
|
```
|
|
305
327
|
|
|
306
|
-
If `
|
|
328
|
+
If `regressions.file` is configured, `testkit` enriches
|
|
307
329
|
`.testkit/results/latest.json` and `testkit.status.json` with:
|
|
308
330
|
|
|
309
331
|
- per-file `failureDetails`
|
|
310
|
-
- per-file `
|
|
311
|
-
- top-level `
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
- `
|
|
316
|
-
-
|
|
317
|
-
|
|
318
|
-
- `description`
|
|
319
|
-
- product-local bug slice or route-family summary
|
|
320
|
-
- this is where file-specific nuance belongs
|
|
321
|
-
- `whyFailing`
|
|
332
|
+
- per-file `diagnosis` metadata (new regression, known regression, fixed known regression)
|
|
333
|
+
- top-level `regressions` summary and prepared draft updates
|
|
334
|
+
|
|
335
|
+
Regression-catalog entry authoring uses this contract:
|
|
336
|
+
|
|
337
|
+
- `summary`
|
|
338
|
+
- concise local statement of the regression slice
|
|
339
|
+
- `cause`
|
|
322
340
|
- underlying technical cause of the failure
|
|
341
|
+
- `fingerprints`
|
|
342
|
+
- selectors that let testkit automatically recognize the regression in future runs
|
|
323
343
|
|
|
324
|
-
If `
|
|
325
|
-
|
|
326
|
-
|
|
344
|
+
If `regressions.sync` is also configured, `testkit` syncs linked GitHub issues and
|
|
345
|
+
adds top-level regression catalog health to the run/status artifacts. The most
|
|
346
|
+
important catalog-staleness signal is:
|
|
327
347
|
|
|
328
|
-
- a known
|
|
348
|
+
- a known regression still fails, but the linked GitHub issue is closed
|
|
329
349
|
|
|
330
|
-
In `mode: "error"`,
|
|
331
|
-
failure:
|
|
350
|
+
In `mode: "error"`, catalog health can also fail the run for problems such as:
|
|
332
351
|
|
|
333
|
-
-
|
|
334
|
-
-
|
|
352
|
+
- closed issues that still reproduce
|
|
353
|
+
- missing issue refs
|
|
354
|
+
- validation unavailability
|
|
335
355
|
|
|
336
356
|
Reproduction warnings are execution-aware:
|
|
337
357
|
|
|
338
|
-
- `failed` means the known
|
|
358
|
+
- `failed` means the known regression reproduced
|
|
339
359
|
- `passed` means the matched test executed and did not reproduce
|
|
340
360
|
- `skipped` and `not_run` do not count as reproduction evidence
|
|
341
361
|
|
|
342
|
-
Known-failure operations are also available as first-class CLI commands:
|
|
343
|
-
|
|
344
|
-
- `testkit known-failures validate`
|
|
345
|
-
- `testkit known-failures render`
|
|
346
|
-
|
|
347
362
|
## Authoring
|
|
348
363
|
|
|
349
364
|
HTTP suites:
|
|
@@ -365,32 +380,39 @@ Named HTTP profiles live in `testkit.config.ts` and can be referenced by name:
|
|
|
365
380
|
|
|
366
381
|
```ts
|
|
367
382
|
import { defineHttpSuite } from "@elench/testkit";
|
|
368
|
-
import {
|
|
383
|
+
import { auth, defineConfig } from "@elench/testkit/config";
|
|
384
|
+
|
|
385
|
+
const appAuth = auth.fixture({
|
|
386
|
+
contract: auth.contracts.jsonSession({
|
|
387
|
+
authCookie: "session",
|
|
388
|
+
organizationIdPath: "data.organizations[0].id",
|
|
389
|
+
}),
|
|
390
|
+
topology: auth.topologies.crossOrg({
|
|
391
|
+
namespace: "example-app",
|
|
392
|
+
actors: {
|
|
393
|
+
primary: { org: "primary" },
|
|
394
|
+
reviewer: { org: "primary" },
|
|
395
|
+
outsider: { org: "secondary" },
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
369
399
|
|
|
370
400
|
export default defineConfig({
|
|
371
401
|
profiles: {
|
|
372
|
-
http: {
|
|
373
|
-
defaultAuth:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
session: {
|
|
381
|
-
authCookie: "session",
|
|
382
|
-
},
|
|
383
|
-
headers: {
|
|
384
|
-
contentTypeJson: true,
|
|
385
|
-
forwardedFor: "deterministic",
|
|
386
|
-
},
|
|
387
|
-
}).session(),
|
|
388
|
-
},
|
|
402
|
+
http: appAuth.profiles({
|
|
403
|
+
defaultAuth: auth.profile.actor("primary"),
|
|
404
|
+
reviewers: auth.profile.actors({
|
|
405
|
+
actors: ["reviewer", "outsider"],
|
|
406
|
+
primaryActor: "reviewer",
|
|
407
|
+
}),
|
|
408
|
+
raw: auth.profile.raw(),
|
|
409
|
+
}),
|
|
389
410
|
},
|
|
390
411
|
});
|
|
391
412
|
|
|
392
|
-
const suite = defineHttpSuite({ profile: "defaultAuth" }, ({
|
|
393
|
-
req("
|
|
413
|
+
const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ actor, req }) => {
|
|
414
|
+
req.get("/api/auth/session");
|
|
415
|
+
actor?.req.get("/api/auth/session");
|
|
394
416
|
});
|
|
395
417
|
```
|
|
396
418
|
|
|
@@ -440,7 +462,7 @@ Consumers should not set local timeout values in test files.
|
|
|
440
462
|
import { waitFor } from "@elench/testkit/runtime";
|
|
441
463
|
|
|
442
464
|
const response = waitFor(
|
|
443
|
-
() => req("
|
|
465
|
+
() => req.get("/api/v1/jobs/123"),
|
|
444
466
|
(res) => JSON.parse(res.body).data?.status === "completed",
|
|
445
467
|
{ description: "job 123 to complete" }
|
|
446
468
|
);
|
package/lib/cli/args.mjs
CHANGED
|
@@ -7,29 +7,17 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export const POSITIONAL_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
9
9
|
export const LIFECYCLE = new Set(["status", "destroy", "cleanup"]);
|
|
10
|
-
export const KNOWN_FAILURES_ACTIONS = new Set(["render", "validate"]);
|
|
11
10
|
export const RESERVED = new Set([...POSITIONAL_TYPES, ...LIFECYCLE]);
|
|
12
11
|
|
|
13
12
|
export function resolveCliSelection({ first, second, third }) {
|
|
14
13
|
let lifecycle = null;
|
|
15
14
|
let positionalType = null;
|
|
16
|
-
let knownFailuresAction = null;
|
|
17
15
|
let dbAction = null;
|
|
18
16
|
|
|
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
17
|
if (first === "db") {
|
|
30
18
|
if (second === "snapshot" && third === "capture") {
|
|
31
19
|
dbAction = "snapshot-capture";
|
|
32
|
-
return { lifecycle, positionalType,
|
|
20
|
+
return { lifecycle, positionalType, dbAction };
|
|
33
21
|
}
|
|
34
22
|
throw new Error('Unknown db command. Expected "db snapshot capture".');
|
|
35
23
|
}
|
|
@@ -49,7 +37,7 @@ export function resolveCliSelection({ first, second, third }) {
|
|
|
49
37
|
);
|
|
50
38
|
}
|
|
51
39
|
|
|
52
|
-
return { lifecycle, positionalType,
|
|
40
|
+
return { lifecycle, positionalType, dbAction };
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
export function parseTypeOption(values, positionalType = null) {
|
package/lib/cli/args.test.mjs
CHANGED
|
@@ -19,7 +19,6 @@ describe("cli-args", () => {
|
|
|
19
19
|
})
|
|
20
20
|
).toEqual({
|
|
21
21
|
dbAction: null,
|
|
22
|
-
knownFailuresAction: null,
|
|
23
22
|
lifecycle: null,
|
|
24
23
|
positionalType: "int",
|
|
25
24
|
});
|
|
@@ -34,26 +33,12 @@ describe("cli-args", () => {
|
|
|
34
33
|
})
|
|
35
34
|
).toEqual({
|
|
36
35
|
dbAction: null,
|
|
37
|
-
knownFailuresAction: null,
|
|
38
36
|
lifecycle: "status",
|
|
39
37
|
positionalType: null,
|
|
40
38
|
});
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
it("resolves
|
|
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
|
-
|
|
41
|
+
it("resolves db subcommands", () => {
|
|
57
42
|
expect(
|
|
58
43
|
resolveCliSelection({
|
|
59
44
|
first: "db",
|
|
@@ -62,7 +47,6 @@ describe("cli-args", () => {
|
|
|
62
47
|
})
|
|
63
48
|
).toEqual({
|
|
64
49
|
dbAction: "snapshot-capture",
|
|
65
|
-
knownFailuresAction: null,
|
|
66
50
|
lifecycle: null,
|
|
67
51
|
positionalType: null,
|
|
68
52
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import path from "path";
|
|
2
1
|
import { Flags } from "@oclif/core";
|
|
2
|
+
import path from "path";
|
|
3
3
|
import { loadManagedConfigs } from "../app/configs.mjs";
|
|
4
4
|
import {
|
|
5
5
|
parseFileTimeoutOption,
|
|
@@ -142,25 +142,6 @@ export async function runStatusLike(commandName, flags) {
|
|
|
142
142
|
return { ok: true, results: productResults };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
export function makeKnownFailuresFlags() {
|
|
146
|
-
return {
|
|
147
|
-
...sharedFlags,
|
|
148
|
-
input: Flags.string({
|
|
149
|
-
description: "Known failures JSON path (repo-relative)",
|
|
150
|
-
}),
|
|
151
|
-
output: Flags.string({
|
|
152
|
-
description: "Output path",
|
|
153
|
-
}),
|
|
154
|
-
status: Flags.string({
|
|
155
|
-
description: "Status artifact path",
|
|
156
|
-
}),
|
|
157
|
-
"issue-mode": Flags.string({
|
|
158
|
-
description: "Issue validation mode override: off, warn, error",
|
|
159
|
-
options: ["off", "warn", "error"],
|
|
160
|
-
}),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
145
|
export function relativeToProduct(productDir, targetPath) {
|
|
165
146
|
return path.relative(productDir, targetPath);
|
|
166
147
|
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -12,7 +12,6 @@ export function normalizeCliArgs(argv) {
|
|
|
12
12
|
"typecheck",
|
|
13
13
|
"doctor",
|
|
14
14
|
"browser",
|
|
15
|
-
"known-failures",
|
|
16
15
|
"db",
|
|
17
16
|
"help",
|
|
18
17
|
"--help",
|
|
@@ -74,9 +73,6 @@ function findPositionals(args, flagsWithValues) {
|
|
|
74
73
|
|
|
75
74
|
function reorderCommandArgs(args, positionals) {
|
|
76
75
|
const commandTokens = [positionals[0]];
|
|
77
|
-
if (positionals[0]?.value === "known-failures" && positionals[1]) {
|
|
78
|
-
commandTokens.push(positionals[1]);
|
|
79
|
-
}
|
|
80
76
|
if (positionals[0]?.value === "db" && positionals[1] && positionals[2]) {
|
|
81
77
|
commandTokens.push(positionals[1], positionals[2]);
|
|
82
78
|
}
|
|
@@ -70,7 +70,7 @@ export function colorResultLine(line) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export function colorSectionLine(line) {
|
|
73
|
-
if (line === "
|
|
73
|
+
if (line === "Catalog issues:" || line === "Diagnosis:") {
|
|
74
74
|
return pc.bold(line);
|
|
75
75
|
}
|
|
76
76
|
if (/^Summary: /.test(line)) return line.replace("Summary:", `${pc.bold("Summary:")}`);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { buildFailurePresentation } from "../../runner/formatting.mjs";
|
|
2
2
|
import { renderIndentedBlock } from "./terminal-layout.mjs";
|
|
3
3
|
|
|
4
|
-
export function renderFailureBlock(task, outcome, { width,
|
|
4
|
+
export function renderFailureBlock(task, outcome, { width, regressionCatalog } = {}) {
|
|
5
5
|
const presentation = buildFailurePresentation(
|
|
6
6
|
{
|
|
7
7
|
service: task.serviceName,
|
|
8
|
-
type:
|
|
8
|
+
type: normalizeRegressionType(task),
|
|
9
9
|
path: task.file,
|
|
10
10
|
error: outcome.error || null,
|
|
11
11
|
failureDetails: Array.isArray(outcome.failureDetails) ? outcome.failureDetails : [],
|
|
12
12
|
suiteError: null,
|
|
13
13
|
},
|
|
14
|
-
|
|
14
|
+
regressionCatalog
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
const lines = [];
|
|
@@ -24,7 +24,7 @@ export function renderFailureBlock(task, outcome, { width, knownFailures } = {})
|
|
|
24
24
|
return lines;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
27
|
+
function normalizeRegressionType(task) {
|
|
28
28
|
if (task.framework === "playwright") return "pw";
|
|
29
29
|
if (task.type === "integration") return "int";
|
|
30
30
|
return task.type;
|
|
@@ -13,15 +13,15 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
13
13
|
const mode = outputMode || "compact";
|
|
14
14
|
let completedCount = 0;
|
|
15
15
|
let totalFileCount = 0;
|
|
16
|
-
let
|
|
16
|
+
let regressionCatalog = null;
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
outputMode: mode,
|
|
20
20
|
setTotalFileCount(count) {
|
|
21
21
|
totalFileCount = count;
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
setRegressionCatalog(document) {
|
|
24
|
+
regressionCatalog = document;
|
|
25
25
|
},
|
|
26
26
|
writeLine(line = "") {
|
|
27
27
|
stdout.write(`${line}\n`);
|
|
@@ -92,7 +92,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
92
92
|
if (status === "FAIL") {
|
|
93
93
|
const detailLines = renderFailureBlock(task, outcome, {
|
|
94
94
|
width: getTerminalWidth(stdout, 100),
|
|
95
|
-
|
|
95
|
+
regressionCatalog,
|
|
96
96
|
});
|
|
97
97
|
for (const line of detailLines) {
|
|
98
98
|
stdout.write(`${line}\n`);
|
|
@@ -108,15 +108,15 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
108
108
|
if (mode === "json") return;
|
|
109
109
|
stdout.write(`${message}\n`);
|
|
110
110
|
},
|
|
111
|
-
runSummary(results, durationMs,
|
|
111
|
+
runSummary(results, durationMs, regressionReport = null) {
|
|
112
112
|
if (mode === "debug") {
|
|
113
|
-
const lines = buildDebugRunSummaryLines(results, durationMs,
|
|
113
|
+
const lines = buildDebugRunSummaryLines(results, durationMs, regressionReport);
|
|
114
114
|
stdout.write("\n");
|
|
115
115
|
for (const line of lines) stdout.write(`${colorSectionLine(line)}\n`);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const summary = buildRunSummaryData(results, durationMs,
|
|
119
|
+
const summary = buildRunSummaryData(results, durationMs, regressionReport);
|
|
120
120
|
const rows = [
|
|
121
121
|
["Result", summary.result],
|
|
122
122
|
["Passed", String(summary.passed)],
|
|
@@ -129,8 +129,22 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
129
129
|
if (summary.serviceErrors > 0) {
|
|
130
130
|
rows.push(["Runtime errors", String(summary.serviceErrors)]);
|
|
131
131
|
}
|
|
132
|
-
if (summary.
|
|
133
|
-
rows.push(["
|
|
132
|
+
if (summary.newRegressions > 0) {
|
|
133
|
+
rows.push(["New regressions", String(summary.newRegressions)]);
|
|
134
|
+
}
|
|
135
|
+
if (summary.knownRegressions > 0) {
|
|
136
|
+
rows.push(["Known regressions", String(summary.knownRegressions)]);
|
|
137
|
+
}
|
|
138
|
+
if (summary.fixedKnownRegressions > 0) {
|
|
139
|
+
rows.push(["Fixed known", String(summary.fixedKnownRegressions)]);
|
|
140
|
+
}
|
|
141
|
+
if (summary.catalogStale > 0) {
|
|
142
|
+
rows.push(["Catalog stale", String(summary.catalogStale)]);
|
|
143
|
+
}
|
|
144
|
+
if (summary.catalogSyncUnavailable) {
|
|
145
|
+
rows.push(["Catalog sync", "Unavailable"]);
|
|
146
|
+
} else if (summary.usedStaleCache) {
|
|
147
|
+
rows.push(["Catalog sync", "Used stale cache"]);
|
|
134
148
|
}
|
|
135
149
|
const boxed = renderSummaryBox(rows, { stdout });
|
|
136
150
|
stdout.write("\n");
|
|
@@ -127,7 +127,7 @@ describe("run reporter task output", () => {
|
|
|
127
127
|
expect(lines[0]).toContain(`${figures.cross} FAIL api int __testkit__/health/health.int.testkit.ts 4s`);
|
|
128
128
|
expect(lines[1]).toBe(" GET /health expected 200, got 404");
|
|
129
129
|
expect(lines[2]).toContain(' response: {"error":"route not found"}');
|
|
130
|
-
expect(lines[3]).toBe("
|
|
130
|
+
expect(lines[3]).toBe(" regression: new");
|
|
131
131
|
expect(lines[4]).toBe(" logs: requestId=req-1");
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -161,10 +161,12 @@ describe("run reporter task output", () => {
|
|
|
161
161
|
20_000,
|
|
162
162
|
{
|
|
163
163
|
summary: {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
newRegressions: 1,
|
|
165
|
+
knownRegressions: 2,
|
|
166
|
+
fixedKnownRegressions: 3,
|
|
167
|
+
catalogStale: 4,
|
|
168
|
+
catalogSyncUnavailable: false,
|
|
169
|
+
usedStaleCache: true,
|
|
168
170
|
},
|
|
169
171
|
}
|
|
170
172
|
);
|
|
@@ -179,7 +181,11 @@ describe("run reporter task output", () => {
|
|
|
179
181
|
expect(output).toContain("│ Not run");
|
|
180
182
|
expect(output).toContain("│ Files");
|
|
181
183
|
expect(output).toContain("│ Duration");
|
|
182
|
-
expect(output).toContain("│
|
|
184
|
+
expect(output).toContain("│ New regressions");
|
|
185
|
+
expect(output).toContain("│ Known regressions");
|
|
186
|
+
expect(output).toContain("│ Fixed known");
|
|
187
|
+
expect(output).toContain("│ Catalog stale");
|
|
188
|
+
expect(output).toContain("│ Catalog sync");
|
|
183
189
|
expect(output).not.toContain("Failures:");
|
|
184
190
|
expect(output).not.toContain("Runtime Errors:");
|
|
185
191
|
});
|
|
@@ -31,13 +31,13 @@ describe("summary box", () => {
|
|
|
31
31
|
|
|
32
32
|
it("wraps long values instead of widening to content length", () => {
|
|
33
33
|
const lines = renderSummaryBox(
|
|
34
|
-
[["
|
|
34
|
+
[["Catalog sync", "Used stale GitHub cache while catalog validation was unavailable"]],
|
|
35
35
|
{ stdout: createStream(40) }
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
expect(lines.length).toBeGreaterThan(4);
|
|
39
|
-
expect(lines.join("\n")).toContain("
|
|
40
|
-
expect(lines.join("\n")).toContain("
|
|
41
|
-
expect(lines.join("\n")).toContain("
|
|
39
|
+
expect(lines.join("\n")).toContain("Catalog sync");
|
|
40
|
+
expect(lines.join("\n")).toContain("stale");
|
|
41
|
+
expect(lines.join("\n")).toContain("cache");
|
|
42
42
|
});
|
|
43
43
|
});
|
package/lib/cli/viewer.mjs
CHANGED
|
@@ -109,11 +109,11 @@ export function formatFileDetail(productDir, runArtifact, subject, options = {})
|
|
|
109
109
|
for (const line of codeFrame) lines.push(` ${line}`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (subject.file.
|
|
112
|
+
if (subject.file.diagnosis) {
|
|
113
113
|
lines.push("");
|
|
114
|
-
lines.push("
|
|
115
|
-
const
|
|
116
|
-
for (const line of
|
|
114
|
+
lines.push("Diagnosis:");
|
|
115
|
+
const diagnosisLines = formatDiagnosis(subject.file.diagnosis);
|
|
116
|
+
for (const line of diagnosisLines) lines.push(` ${line}`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const setupOperations = getSetupOperationsForService(runArtifact, subject.service.name);
|
|
@@ -290,30 +290,29 @@ function formatResponseLine(detail) {
|
|
|
290
290
|
return parts.join(" ");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
function
|
|
294
|
-
const lines = [`status: ${
|
|
295
|
-
if (
|
|
296
|
-
lines.push(`classification: ${
|
|
293
|
+
function formatDiagnosis(diagnosis) {
|
|
294
|
+
const lines = [`status: ${diagnosis.status}`];
|
|
295
|
+
if (diagnosis.classifications?.length) {
|
|
296
|
+
lines.push(`classification: ${diagnosis.classifications.join(", ")}`);
|
|
297
297
|
}
|
|
298
|
-
|
|
299
|
-
lines.push(
|
|
300
|
-
`validation: ${triage.availability.mode}${triage.availability.reason ? ` (${triage.availability.reason})` : ""}`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
for (const entry of triage.entries || []) {
|
|
298
|
+
for (const entry of diagnosis.entries || []) {
|
|
304
299
|
lines.push(`issue: ${entry.issue.repo}#${entry.issue.number}`);
|
|
305
|
-
|
|
300
|
+
lines.push(`summary: ${entry.summary}`);
|
|
301
|
+
if (entry.github?.url) lines.push(`url: ${entry.github.url}`);
|
|
306
302
|
if (entry.github?.state) {
|
|
307
303
|
lines.push(`github state: ${entry.github.state}${entry.github.cached ? " (cache)" : ""}`);
|
|
308
304
|
}
|
|
309
|
-
if (entry.
|
|
310
|
-
if (entry.
|
|
311
|
-
for (const finding of entry.
|
|
312
|
-
lines.push(`finding: ${finding.message}`);
|
|
305
|
+
if (entry.syncStatus) lines.push(`catalog status: ${entry.syncStatus}`);
|
|
306
|
+
if (entry.catalogFindings?.length) {
|
|
307
|
+
for (const finding of entry.catalogFindings.slice(0, 3)) {
|
|
308
|
+
lines.push(`catalog finding: ${finding.message}`);
|
|
313
309
|
}
|
|
314
310
|
}
|
|
315
311
|
break;
|
|
316
312
|
}
|
|
313
|
+
if (diagnosis.status === "new_regression") {
|
|
314
|
+
lines.push("next: review .testkit/results/latest.json drafts.newRegressions for a prepared catalog entry");
|
|
315
|
+
}
|
|
317
316
|
return lines;
|
|
318
317
|
}
|
|
319
318
|
|
package/lib/config/index.mjs
CHANGED
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
detectNextApp,
|
|
15
15
|
inferLocalRuntime,
|
|
16
16
|
normalizeBrowserServiceConfig,
|
|
17
|
+
normalizeRegressionsConfig,
|
|
17
18
|
normalizeOptionalString,
|
|
18
19
|
normalizeRepoExecution,
|
|
19
|
-
normalizeReportingConfig,
|
|
20
20
|
normalizeRuntimeConfig,
|
|
21
21
|
} from "./runtime.mjs";
|
|
22
22
|
import { normalizeServiceRequirements, normalizeSkipConfig } from "./skip-config.mjs";
|
|
@@ -30,7 +30,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
30
30
|
const configContext = opts.configContext || (await loadTestkitConfig(productDir));
|
|
31
31
|
const { config, configFile } = configContext;
|
|
32
32
|
const execution = normalizeRepoExecution(config.execution);
|
|
33
|
-
const
|
|
33
|
+
const regressions = normalizeRegressionsConfig(config.regressions);
|
|
34
34
|
const toolchains = normalizeToolchainRegistry(config.toolchains);
|
|
35
35
|
const discoveryConfig = normalizeRepoDiscoveryConfig(config.discovery);
|
|
36
36
|
const explicitServices = config.services || {};
|
|
@@ -54,7 +54,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
54
54
|
configFile,
|
|
55
55
|
execution,
|
|
56
56
|
discovery: discoveryConfig,
|
|
57
|
-
|
|
57
|
+
regressions,
|
|
58
58
|
toolchains,
|
|
59
59
|
explicitService: explicitServices[name] || {},
|
|
60
60
|
discoveredService: discovery.services[name] || null,
|
|
@@ -71,7 +71,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
71
71
|
configFile,
|
|
72
72
|
execution,
|
|
73
73
|
discovery: discoveryConfig,
|
|
74
|
-
|
|
74
|
+
regressions,
|
|
75
75
|
toolchains,
|
|
76
76
|
explicitServices,
|
|
77
77
|
discovery,
|
|
@@ -102,7 +102,7 @@ function normalizeServiceConfig({
|
|
|
102
102
|
configFile,
|
|
103
103
|
execution,
|
|
104
104
|
discovery,
|
|
105
|
-
|
|
105
|
+
regressions,
|
|
106
106
|
toolchains,
|
|
107
107
|
explicitService,
|
|
108
108
|
discoveredService,
|
|
@@ -156,7 +156,7 @@ function normalizeServiceConfig({
|
|
|
156
156
|
suites,
|
|
157
157
|
testkit: {
|
|
158
158
|
execution,
|
|
159
|
-
|
|
159
|
+
regressions,
|
|
160
160
|
dependsOn: explicitService.dependsOn || [],
|
|
161
161
|
discovery: normalizedDiscovery,
|
|
162
162
|
database,
|
package/lib/config/runtime.mjs
CHANGED
|
@@ -15,23 +15,23 @@ import {
|
|
|
15
15
|
parseModuleSpecifier,
|
|
16
16
|
} from "../shared/configured-steps.mjs";
|
|
17
17
|
import { buildConfigToPrepare, normalizeBuildConfig } from "../shared/build-config.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { normalizeRegressionSyncConfig } from "../regressions/github.mjs";
|
|
19
19
|
import { normalizeRuntimeToolchain } from "../toolchains/index.mjs";
|
|
20
20
|
import { resolveServiceCwd } from "./paths.mjs";
|
|
21
21
|
|
|
22
|
-
export function
|
|
22
|
+
export function normalizeRegressionsConfig(value) {
|
|
23
23
|
if (!value) return null;
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
throw new Error('testkit.config.ts
|
|
25
|
+
const file = normalizeOptionalString(value.file);
|
|
26
|
+
if (!file) {
|
|
27
|
+
throw new Error('testkit.config.ts regressions.file must be a non-empty string');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const sync = normalizeRegressionSyncConfig(value.sync);
|
|
31
31
|
|
|
32
32
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
file,
|
|
34
|
+
sync,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|