@elench/testkit 0.1.79 → 0.1.81

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 (47) hide show
  1. package/README.md +50 -35
  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 +31 -0
  8. package/lib/cli/presentation/run-reporter.mjs +63 -93
  9. package/lib/cli/presentation/run-reporter.test.mjs +137 -26
  10. package/lib/cli/presentation/summary-box.mjs +45 -0
  11. package/lib/cli/presentation/summary-box.test.mjs +43 -0
  12. package/lib/cli/presentation/terminal-layout.mjs +43 -0
  13. package/lib/cli/presentation/terminal-layout.test.mjs +23 -0
  14. package/lib/cli/viewer.mjs +18 -19
  15. package/lib/config/index.mjs +6 -6
  16. package/lib/config/runtime.mjs +8 -8
  17. package/lib/config-api/index.d.ts +4 -4
  18. package/lib/package.test.mjs +4 -4
  19. package/lib/{known-failures → regressions}/github.mjs +39 -77
  20. package/lib/regressions/github.test.mjs +324 -0
  21. package/lib/regressions/index.d.ts +189 -0
  22. package/lib/{known-failures → regressions}/index.mjs +90 -93
  23. package/lib/{known-failures → regressions}/index.test.mjs +37 -48
  24. package/lib/runner/formatting.mjs +105 -103
  25. package/lib/runner/formatting.test.mjs +94 -131
  26. package/lib/runner/metadata.mjs +1 -1
  27. package/lib/runner/orchestrator.mjs +7 -8
  28. package/lib/runner/regressions.mjs +304 -0
  29. package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
  30. package/lib/runner/reporting.mjs +2 -2
  31. package/lib/runner/reporting.test.mjs +2 -2
  32. package/lib/runner/run-finalization.mjs +18 -30
  33. package/lib/runner/template-steps.mjs +2 -2
  34. package/lib/runner/worker-loop.mjs +1 -0
  35. package/node_modules/@elench/next-analysis/package.json +1 -1
  36. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  37. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  38. package/node_modules/@elench/ts-analysis/package.json +1 -1
  39. package/package.json +12 -9
  40. package/lib/cli/commands/known-failures/render.mjs +0 -19
  41. package/lib/cli/commands/known-failures/validate.mjs +0 -20
  42. package/lib/cli/known-failures.mjs +0 -164
  43. package/lib/known-failures/github.test.mjs +0 -512
  44. package/lib/known-failures/index.d.ts +0 -192
  45. package/lib/runner/triage.mjs +0 -221
  46. /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
  47. /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:
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 === "Failures:" || line === "Runtime Errors:" || 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:")}`);
@@ -0,0 +1,31 @@
1
+ import { buildFailurePresentation } from "../../runner/formatting.mjs";
2
+ import { renderIndentedBlock } from "./terminal-layout.mjs";
3
+
4
+ export function renderFailureBlock(task, outcome, { width, regressionCatalog } = {}) {
5
+ const presentation = buildFailurePresentation(
6
+ {
7
+ service: task.serviceName,
8
+ type: normalizeRegressionType(task),
9
+ path: task.file,
10
+ error: outcome.error || null,
11
+ failureDetails: Array.isArray(outcome.failureDetails) ? outcome.failureDetails : [],
12
+ suiteError: null,
13
+ },
14
+ regressionCatalog
15
+ );
16
+
17
+ const lines = [];
18
+ if (presentation.primary) {
19
+ lines.push(...renderIndentedBlock(presentation.primary, { width, indent: " " }));
20
+ }
21
+ for (const detail of presentation.details) {
22
+ lines.push(...renderIndentedBlock(detail, { width, indent: " " }));
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ function normalizeRegressionType(task) {
28
+ if (task.framework === "playwright") return "pw";
29
+ if (task.type === "integration") return "int";
30
+ return task.type;
31
+ }
@@ -1,89 +1,28 @@
1
1
  import path from "path";
2
- import figures from "figures";
3
2
  import {
4
- buildCompactRunSummaryLines,
3
+ buildRunSummaryData,
5
4
  buildDebugRunSummaryLines,
6
5
  formatDuration,
7
6
  } from "../../runner/formatting.mjs";
8
- import { boldRed, colorSectionLine, dim, red, statusLabel } from "./colors.mjs";
9
-
10
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
11
- const DURATION_RE = /\b(\d+m\s+\d+s|\d+s)\b/g;
12
-
13
- function stripAnsi(text) {
14
- return text.replace(ANSI_RE, "");
15
- }
16
-
17
- function boxLines(lines) {
18
- const nonEmpty = lines.filter((l) => stripAnsi(l).trim().length > 0);
19
- if (nonEmpty.length === 0) return lines;
20
- const maxWidth = lines.reduce((max, l) => Math.max(max, stripAnsi(l).length), 0);
21
- const pad = 1;
22
- const innerWidth = maxWidth + pad * 2;
23
- const top = `${figures.lineDownRight}${figures.line.repeat(innerWidth)}${figures.lineDownLeft}`;
24
- const bottom = `${figures.lineUpRight}${figures.line.repeat(innerWidth)}${figures.lineUpLeft}`;
25
- const boxed = [top];
26
- for (const line of lines) {
27
- const visible = stripAnsi(line).length;
28
- const right = innerWidth - pad - visible;
29
- boxed.push(`${figures.lineVertical}${" ".repeat(pad)}${line}${" ".repeat(Math.max(0, right))}${figures.lineVertical}`);
30
- }
31
- boxed.push(bottom);
32
- return boxed;
33
- }
34
-
35
- function colorFailureSummaryLines(lines) {
36
- const result = [];
37
- let inFailuresSection = false;
38
- let lastWasFilePath = false;
39
- for (const line of lines) {
40
- const raw = stripAnsi(line);
41
- if (raw === "Failures:" || raw === "Runtime Errors:") {
42
- inFailuresSection = true;
43
- lastWasFilePath = false;
44
- result.push(line);
45
- continue;
46
- }
47
- if (/^(Summary:|Result:|Known-failure|Triage:)/.test(raw) || raw === "") {
48
- inFailuresSection = false;
49
- lastWasFilePath = false;
50
- }
51
- if (inFailuresSection) {
52
- if (/^ {2}\S/.test(raw)) {
53
- lastWasFilePath = true;
54
- result.push(boldRed(raw));
55
- continue;
56
- }
57
- if (/^ {4}\S/.test(raw)) {
58
- if (lastWasFilePath) {
59
- lastWasFilePath = false;
60
- result.push(boldRed(raw));
61
- } else {
62
- result.push(red(raw));
63
- }
64
- continue;
65
- }
66
- }
67
- lastWasFilePath = false;
68
- result.push(line);
69
- }
70
- return result;
71
- }
72
-
73
- function dimDurations(line) {
74
- return line.replace(DURATION_RE, (match) => dim(match));
75
- }
7
+ import { boldRed, colorSectionLine, dim, statusLabel } from "./colors.mjs";
8
+ import { renderFailureBlock } from "./failure-presentation.mjs";
9
+ import { renderSummaryBox } from "./summary-box.mjs";
10
+ import { getTerminalWidth } from "./terminal-layout.mjs";
76
11
 
77
12
  export function createRunReporter({ outputMode = "compact", stdout = process.stdout, stderr = process.stderr } = {}) {
78
13
  const mode = outputMode || "compact";
79
14
  let completedCount = 0;
80
15
  let totalFileCount = 0;
16
+ let regressionCatalog = null;
81
17
 
82
18
  return {
83
19
  outputMode: mode,
84
20
  setTotalFileCount(count) {
85
21
  totalFileCount = count;
86
22
  },
23
+ setRegressionCatalog(document) {
24
+ regressionCatalog = document;
25
+ },
87
26
  writeLine(line = "") {
88
27
  stdout.write(`${line}\n`);
89
28
  },
@@ -142,32 +81,72 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
142
81
  completedCount += 1;
143
82
  const status = outcome.status === "skipped" ? "SKIP" : outcome.failed ? "FAIL" : "PASS";
144
83
  const duration = formatDuration(outcome.durationMs || 0);
145
- const primaryFailure = firstFailureDetail(outcome);
146
- const preferredFailure =
147
- primaryFailure && isThresholdWrapperMessage(outcome.error) ? primaryFailure : outcome.error || primaryFailure;
148
84
  const detail =
149
- status === "FAIL"
150
- ? ` ${boldRed(shortenMessage(preferredFailure || "failed"))}`
151
- : outcome.status === "not_run"
85
+ outcome.status === "not_run"
152
86
  ? ` ${dim(shortenMessage(outcome.reason || "not run"))}`
153
87
  : "";
154
88
  const progress = mode === "compact" && totalFileCount > 0 ? `${dim(`[${completedCount}/${totalFileCount}]`)} ` : "";
155
89
  stdout.write(
156
90
  `${progress}${statusLabel(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
157
91
  );
92
+ if (status === "FAIL") {
93
+ const detailLines = renderFailureBlock(task, outcome, {
94
+ width: getTerminalWidth(stdout, 100),
95
+ regressionCatalog,
96
+ });
97
+ for (const line of detailLines) {
98
+ stdout.write(`${line}\n`);
99
+ }
100
+ }
101
+ },
102
+ runtimeError(task, message) {
103
+ if (mode === "json") return;
104
+ stdout.write(`${statusLabel("FAIL")} ${task.serviceName} runtime error\n`);
105
+ stdout.write(` ${boldRed(shortenMessage(message || "runtime error"))}\n`);
158
106
  },
159
107
  telemetry(message) {
160
108
  if (mode === "json") return;
161
109
  stdout.write(`${message}\n`);
162
110
  },
163
- runSummary(results, durationMs, knownFailureIssueValidation = null) {
164
- const lines =
165
- mode === "debug"
166
- ? buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation)
167
- : buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
168
- const colored = colorFailureSummaryLines(lines.map((line) => colorSectionLine(line)));
169
- const dimmed = colored.map((line) => dimDurations(line));
170
- const boxed = boxLines(dimmed);
111
+ runSummary(results, durationMs, regressionReport = null) {
112
+ if (mode === "debug") {
113
+ const lines = buildDebugRunSummaryLines(results, durationMs, regressionReport);
114
+ stdout.write("\n");
115
+ for (const line of lines) stdout.write(`${colorSectionLine(line)}\n`);
116
+ return;
117
+ }
118
+
119
+ const summary = buildRunSummaryData(results, durationMs, regressionReport);
120
+ const rows = [
121
+ ["Result", summary.result],
122
+ ["Passed", String(summary.passed)],
123
+ ["Failed", String(summary.failed)],
124
+ ["Skipped", String(summary.skipped)],
125
+ ["Not run", String(summary.notRun)],
126
+ ["Files", String(summary.files)],
127
+ ["Duration", summary.duration],
128
+ ];
129
+ if (summary.serviceErrors > 0) {
130
+ rows.push(["Runtime errors", String(summary.serviceErrors)]);
131
+ }
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"]);
148
+ }
149
+ const boxed = renderSummaryBox(rows, { stdout });
171
150
  stdout.write("\n");
172
151
  for (const line of boxed) stdout.write(`${line}\n`);
173
152
  },
@@ -187,15 +166,6 @@ function shortenMessage(message) {
187
166
  return String(message).replace(/\s+/g, " ").trim().slice(0, 180);
188
167
  }
189
168
 
190
- function firstFailureDetail(outcome) {
191
- const detail = outcome.failureDetails?.[0];
192
- return detail?.message || detail?.title || null;
193
- }
194
-
195
- function isThresholdWrapperMessage(message) {
196
- return /Default runtime thresholds failed:/.test(String(message || ""));
197
- }
198
-
199
169
  function normalizePath(filePath) {
200
170
  return String(filePath).split(path.sep).join("/");
201
171
  }