@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.
Files changed (54) hide show
  1. package/README.md +78 -56
  2. package/lib/cli/args.mjs +2 -14
  3. package/lib/cli/args.test.mjs +1 -17
  4. package/lib/cli/command-helpers.mjs +1 -20
  5. package/lib/cli/entrypoint.mjs +0 -4
  6. package/lib/cli/presentation/colors.mjs +1 -1
  7. package/lib/cli/presentation/failure-presentation.mjs +4 -4
  8. package/lib/cli/presentation/run-reporter.mjs +23 -9
  9. package/lib/cli/presentation/run-reporter.test.mjs +12 -6
  10. package/lib/cli/presentation/summary-box.test.mjs +4 -4
  11. package/lib/cli/viewer.mjs +18 -19
  12. package/lib/config/index.mjs +6 -6
  13. package/lib/config/runtime.mjs +8 -8
  14. package/lib/config-api/auth-fixtures.mjs +762 -0
  15. package/lib/config-api/index.d.ts +96 -112
  16. package/lib/config-api/index.mjs +22 -12
  17. package/lib/config-api/index.test.mjs +61 -222
  18. package/lib/index.d.ts +29 -9
  19. package/lib/package.test.mjs +4 -4
  20. package/lib/{known-failures → regressions}/github.mjs +36 -78
  21. package/lib/regressions/github.test.mjs +324 -0
  22. package/lib/regressions/index.d.ts +189 -0
  23. package/lib/{known-failures → regressions}/index.mjs +90 -93
  24. package/lib/{known-failures → regressions}/index.test.mjs +37 -48
  25. package/lib/runner/formatting.mjs +49 -34
  26. package/lib/runner/formatting.test.mjs +16 -15
  27. package/lib/runner/metadata.mjs +1 -1
  28. package/lib/runner/orchestrator.mjs +7 -9
  29. package/lib/runner/regressions.mjs +304 -0
  30. package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
  31. package/lib/runner/reporting.mjs +2 -2
  32. package/lib/runner/reporting.test.mjs +2 -2
  33. package/lib/runner/run-finalization.mjs +18 -30
  34. package/lib/runner/template-steps.mjs +2 -2
  35. package/lib/runtime/index.d.ts +50 -33
  36. package/lib/runtime/index.mjs +0 -1
  37. package/lib/runtime-src/k6/http-suite-runtime.js +147 -0
  38. package/lib/runtime-src/k6/http.js +80 -41
  39. package/lib/runtime-src/k6/scenario-suite.js +13 -110
  40. package/lib/runtime-src/k6/suite.js +13 -107
  41. package/node_modules/@elench/next-analysis/package.json +1 -1
  42. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  43. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  44. package/node_modules/@elench/ts-analysis/package.json +1 -1
  45. package/package.json +8 -8
  46. package/lib/cli/commands/known-failures/render.mjs +0 -19
  47. package/lib/cli/commands/known-failures/validate.mjs +0 -20
  48. package/lib/cli/known-failures.mjs +0 -164
  49. package/lib/config-api/profiles.mjs +0 -640
  50. package/lib/known-failures/github.test.mjs +0 -512
  51. package/lib/known-failures/index.d.ts +0 -192
  52. package/lib/runner/triage.mjs +0 -221
  53. /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
  54. /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
- # Known-failures tooling
66
- npx @elench/testkit known-failures validate --issue-mode error
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
- reporting: {
163
- knownFailuresFile: "testkit.known-failures.json",
164
- issueValidation: {
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
- - known-failure annotation merge for enriched status/run artifacts
244
- - optional GitHub-backed known-failure issue validation
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 `reporting.knownFailuresFile` is configured, `testkit` enriches
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 `triage` metadata (issue, classification, description)
311
- - top-level `triageSummary` counts for known vs untriaged failures
312
-
313
- Known-failure entry authoring uses this contract:
314
-
315
- - `title`
316
- - exact issue-tracker title for the linked issue
317
- - shared issues may reuse the same title across multiple local entries
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 `reporting.issueValidation` is also configured, `testkit` validates known-failure
325
- issue references against GitHub and adds top-level `knownFailuresIssueValidation`
326
- data to the run/status artifacts. The most important stale-triage signal is:
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-failure test still fails, but the linked GitHub issue is closed
348
+ - a known regression still fails, but the linked GitHub issue is closed
329
349
 
330
- In `mode: "error"`, exact GitHub metadata drift is also treated as a validation
331
- failure:
350
+ In `mode: "error"`, catalog health can also fail the run for problems such as:
332
351
 
333
- - title does not match the linked issue title
334
- - local open/closed state does not match the linked issue state
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 failure reproduced
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 { defineConfig, profiles } from "@elench/testkit/config";
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: profiles.localJson({
374
- password: "password",
375
- identities: {
376
- primary: {
377
- email: "test@example.com",
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" }, ({ req, setupData }) => {
393
- req("GET", "/api/auth/session", setupData);
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("GET", "/api/v1/jobs/123", setupData),
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, knownFailuresAction, dbAction };
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, knownFailuresAction, dbAction };
40
+ return { lifecycle, positionalType, dbAction };
53
41
  }
54
42
 
55
43
  export function parseTypeOption(values, positionalType = null) {
@@ -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 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
-
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
  }
@@ -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 === "Known-failure issues:" || line === "Triage:") {
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, knownFailures } = {}) {
4
+ export function renderFailureBlock(task, outcome, { width, regressionCatalog } = {}) {
5
5
  const presentation = buildFailurePresentation(
6
6
  {
7
7
  service: task.serviceName,
8
- type: normalizeKnownFailureType(task),
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
- knownFailures
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 normalizeKnownFailureType(task) {
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 knownFailures = null;
16
+ let regressionCatalog = null;
17
17
 
18
18
  return {
19
19
  outputMode: mode,
20
20
  setTotalFileCount(count) {
21
21
  totalFileCount = count;
22
22
  },
23
- setKnownFailures(document) {
24
- knownFailures = document;
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
- knownFailures,
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, knownFailureIssueValidation = null) {
111
+ runSummary(results, durationMs, regressionReport = null) {
112
112
  if (mode === "debug") {
113
- const lines = buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation);
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, knownFailureIssueValidation);
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.knownFailureIssues.length > 0) {
133
- rows.push(["Known issues", summary.knownFailureIssues.join(", ")]);
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(" triage: untriaged");
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
- byCode: {
165
- closed_but_failing: 2,
166
- state_mismatch: 1,
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("│ Known issues");
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
- [["Known issues", "2 closed issues still failing, 6 state mismatches, used stale GitHub cache"]],
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("Known issues");
40
- expect(lines.join("\n")).toContain("state");
41
- expect(lines.join("\n")).toContain("mismatches");
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
  });
@@ -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.triage) {
112
+ if (subject.file.diagnosis) {
113
113
  lines.push("");
114
- lines.push("Triage:");
115
- const triageLines = formatTriage(subject.file.triage);
116
- for (const line of triageLines) lines.push(` ${line}`);
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 formatTriage(triage) {
294
- const lines = [`status: ${triage.status}`];
295
- if (triage.classifications?.length) {
296
- lines.push(`classification: ${triage.classifications.join(", ")}`);
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
- if (triage.availability?.mode) {
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
- if (entry.issue.url) lines.push(`url: ${entry.issue.url}`);
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.validationStatus) lines.push(`validation status: ${entry.validationStatus}`);
310
- if (entry.findings?.length) {
311
- for (const finding of entry.findings.slice(0, 3)) {
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
 
@@ -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 reporting = normalizeReportingConfig(config.reporting);
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
- reporting,
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
- reporting,
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
- reporting,
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
- reporting,
159
+ regressions,
160
160
  dependsOn: explicitService.dependsOn || [],
161
161
  discovery: normalizedDiscovery,
162
162
  database,
@@ -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 { normalizeKnownFailureIssueValidationConfig } from "../known-failures/github.mjs";
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 normalizeReportingConfig(value) {
22
+ export function normalizeRegressionsConfig(value) {
23
23
  if (!value) return null;
24
24
 
25
- const knownFailuresFile = normalizeOptionalString(value.knownFailuresFile);
26
- if (!knownFailuresFile) {
27
- throw new Error('testkit.config.ts reporting.knownFailuresFile must be a non-empty string');
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 issueValidation = normalizeKnownFailureIssueValidationConfig(value.issueValidation);
30
+ const sync = normalizeRegressionSyncConfig(value.sync);
31
31
 
32
32
  return {
33
- knownFailuresFile,
34
- issueValidation,
33
+ file,
34
+ sync,
35
35
  };
36
36
  }
37
37