@elench/testkit 0.1.42 → 0.1.44

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.
@@ -0,0 +1,91 @@
1
+ export function normalizeFailureDetail(detail) {
2
+ if (!detail || typeof detail !== "object") return null;
3
+
4
+ const kind = normalizeNonEmptyString(detail.kind);
5
+ const key = normalizeNonEmptyString(detail.key);
6
+ const title = normalizeNonEmptyString(detail.title);
7
+ if (!kind || !key || !title) return null;
8
+
9
+ const normalized = {
10
+ kind,
11
+ key,
12
+ title,
13
+ count: normalizePositiveInteger(detail.count) || 1,
14
+ };
15
+
16
+ const groupPath = normalizeStringArray(detail.groupPath);
17
+ if (groupPath.length > 0) normalized.groupPath = groupPath;
18
+
19
+ const checkName = normalizeNonEmptyString(detail.checkName);
20
+ if (checkName) normalized.checkName = checkName;
21
+
22
+ const suitePath = normalizeStringArray(detail.suitePath);
23
+ if (suitePath.length > 0) normalized.suitePath = suitePath;
24
+
25
+ const phase = normalizeNonEmptyString(detail.phase);
26
+ if (phase) normalized.phase = phase;
27
+
28
+ const message = normalizeNonEmptyString(detail.message);
29
+ if (message) normalized.message = message;
30
+
31
+ return normalized;
32
+ }
33
+
34
+ export function mergeFailureDetails(details) {
35
+ const mergedByKey = new Map();
36
+
37
+ for (const detail of Array.isArray(details) ? details : []) {
38
+ const normalized = normalizeFailureDetail(detail);
39
+ if (!normalized) continue;
40
+ const dedupeKey = `${normalized.kind}::${normalized.key}`;
41
+ const existing = mergedByKey.get(dedupeKey);
42
+ if (!existing) {
43
+ mergedByKey.set(dedupeKey, { ...normalized });
44
+ continue;
45
+ }
46
+ existing.count += normalized.count;
47
+ if (!existing.message && normalized.message) {
48
+ existing.message = normalized.message;
49
+ }
50
+ }
51
+
52
+ return [...mergedByKey.values()].sort(
53
+ (left, right) =>
54
+ left.kind.localeCompare(right.kind) || left.key.localeCompare(right.key)
55
+ );
56
+ }
57
+
58
+ export function collectFailureDetailsFromRuntimeArtifacts(artifacts) {
59
+ const details = [];
60
+
61
+ for (const artifact of Array.isArray(artifacts) ? artifacts : []) {
62
+ if (artifact?.kind !== "testkit.failure-details") continue;
63
+ const phase = normalizeNonEmptyString(artifact?.data?.phase);
64
+ for (const detail of Array.isArray(artifact?.data?.failures) ? artifact.data.failures : []) {
65
+ details.push({
66
+ ...detail,
67
+ phase: detail?.phase || phase || null,
68
+ });
69
+ }
70
+ }
71
+
72
+ return mergeFailureDetails(details);
73
+ }
74
+
75
+ function normalizeStringArray(value) {
76
+ if (!Array.isArray(value)) return [];
77
+ return value
78
+ .map((entry) => normalizeNonEmptyString(entry))
79
+ .filter(Boolean);
80
+ }
81
+
82
+ function normalizeNonEmptyString(value) {
83
+ if (typeof value !== "string") return null;
84
+ const normalized = value.trim();
85
+ return normalized.length > 0 ? normalized : null;
86
+ }
87
+
88
+ function normalizePositiveInteger(value) {
89
+ if (!Number.isInteger(value) || value <= 0) return null;
90
+ return value;
91
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ collectFailureDetailsFromRuntimeArtifacts,
4
+ mergeFailureDetails,
5
+ } from "./failure-details.mjs";
6
+
7
+ describe("runner failure details", () => {
8
+ it("merges duplicate failure details by kind and key", () => {
9
+ expect(
10
+ mergeFailureDetails([
11
+ { kind: "k6-check", key: "group > check", title: "check" },
12
+ { kind: "k6-check", key: "group > check", title: "check" },
13
+ { kind: "playwright-spec", key: "spec title", title: "spec title" },
14
+ ])
15
+ ).toEqual([
16
+ {
17
+ kind: "k6-check",
18
+ key: "group > check",
19
+ title: "check",
20
+ count: 2,
21
+ },
22
+ {
23
+ kind: "playwright-spec",
24
+ key: "spec title",
25
+ title: "spec title",
26
+ count: 1,
27
+ },
28
+ ]);
29
+ });
30
+
31
+ it("extracts and normalizes runtime failure-detail artifacts", () => {
32
+ expect(
33
+ collectFailureDetailsFromRuntimeArtifacts([
34
+ {
35
+ kind: "testkit.failure-details",
36
+ data: {
37
+ phase: "exec",
38
+ failures: [
39
+ {
40
+ kind: "k6-check",
41
+ key: "status is 200",
42
+ title: "status is 200",
43
+ },
44
+ {
45
+ kind: "k6-check",
46
+ key: "status is 200",
47
+ title: "status is 200",
48
+ },
49
+ ],
50
+ },
51
+ },
52
+ ])
53
+ ).toEqual([
54
+ {
55
+ kind: "k6-check",
56
+ key: "status is 200",
57
+ title: "status is 200",
58
+ count: 2,
59
+ phase: "exec",
60
+ },
61
+ ]);
62
+ });
63
+ });
@@ -1,3 +1,5 @@
1
+ import { buildKnownFailureIssueValidationSummaryLines } from "../known-failures/github.mjs";
2
+
1
3
  export function formatDuration(durationMs) {
2
4
  const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
3
5
  const minutes = Math.floor(totalSeconds / 60);
@@ -46,7 +48,7 @@ export function formatSuiteFramework(framework) {
46
48
  return label ? ` [${label}]` : "";
47
49
  }
48
50
 
49
- export function buildRunSummaryLines(results, durationMs) {
51
+ export function buildRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
50
52
  const totalServices = results.length;
51
53
  const executedServices = results.filter((result) => !result.skipped);
52
54
  const skippedServices = results.filter((result) => result.skipped);
@@ -115,6 +117,13 @@ export function buildRunSummaryLines(results, durationMs) {
115
117
  }
116
118
  }
117
119
 
120
+ const knownFailureIssueLines = buildKnownFailureIssueValidationSummaryLines(
121
+ knownFailureIssueValidation
122
+ );
123
+ if (knownFailureIssueLines.length > 0) {
124
+ lines.push(...knownFailureIssueLines);
125
+ }
126
+
118
127
  if (failedServices.length > 0) {
119
128
  lines.push("", `Result: FAILED (${failedServices.length}/${totalServices} services failed)`);
120
129
  return lines;
@@ -128,4 +128,40 @@ describe("runner formatting", () => {
128
128
  expect(lines.join("\n")).toContain("SKIP api");
129
129
  expect(lines.at(-1)).toBe("Result: PASSED");
130
130
  });
131
+
132
+ it("appends known-failure issue validation summary lines", () => {
133
+ const lines = buildRunSummaryLines(
134
+ [
135
+ {
136
+ name: "api",
137
+ skipped: false,
138
+ failed: false,
139
+ suiteCount: 1,
140
+ completedSuiteCount: 1,
141
+ skippedSuiteCount: 0,
142
+ failedSuiteCount: 0,
143
+ totalFileCount: 1,
144
+ passedFileCount: 1,
145
+ skippedFileCount: 0,
146
+ notRunFileCount: 0,
147
+ durationMs: 1_000,
148
+ suites: [],
149
+ errors: [],
150
+ },
151
+ ],
152
+ 1_000,
153
+ {
154
+ summary: {
155
+ byCode: {
156
+ closed_but_failing: 2,
157
+ title_mismatch: 1,
158
+ },
159
+ },
160
+ }
161
+ );
162
+
163
+ expect(lines.join("\n")).toContain("Known-failure issues:");
164
+ expect(lines.join("\n")).toContain("2 closed issues still failing");
165
+ expect(lines.join("\n")).toContain("1 title mismatch");
166
+ });
131
167
  });
@@ -3,6 +3,7 @@ import os from "os";
3
3
  import path from "path";
4
4
  import { execFileSync } from "child_process";
5
5
  import { fileURLToPath } from "url";
6
+ import { parseGitHubRepoSlug } from "../known-failures/github.mjs";
6
7
 
7
8
  export function collectGitMetadata(productDir) {
8
9
  const read = (args) => {
@@ -13,10 +14,14 @@ export function collectGitMetadata(productDir) {
13
14
  }
14
15
  };
15
16
 
17
+ const remoteUrl = read(["remote", "get-url", "origin"]);
18
+
16
19
  return {
17
20
  branch: read(["rev-parse", "--abbrev-ref", "HEAD"]),
18
21
  commitSha: read(["rev-parse", "HEAD"]),
19
22
  repoRoot: read(["rev-parse", "--show-toplevel"]),
23
+ remoteUrl,
24
+ repoSlug: parseGitHubRepoSlug(remoteUrl),
20
25
  };
21
26
  }
22
27
 
@@ -15,7 +15,12 @@ import {
15
15
  summarizeDbBackend,
16
16
  } from "./results.mjs";
17
17
  import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
18
+ import { applyKnownFailuresToArtifacts, loadKnownFailuresConfig } from "./triage.mjs";
18
19
  import { buildRunSummaryLines, formatError } from "./formatting.mjs";
20
+ import {
21
+ shouldFailKnownFailureIssueValidation,
22
+ validateKnownFailureIssues,
23
+ } from "../known-failures/github.mjs";
19
24
  import {
20
25
  loadTimings,
21
26
  resetResultArtifacts,
@@ -56,6 +61,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
56
61
  },
57
62
  testkitVersion: readPackageMetadata().version,
58
63
  };
64
+ const knownFailures = loadKnownFailuresConfig(
65
+ productDir,
66
+ configs[0]?.testkit?.reporting || null
67
+ );
59
68
  const requestedFiles = opts.fileNames || [];
60
69
  if (requestedFiles.length > 0) {
61
70
  const unmatchedFiles = findUnmatchedRequestedFiles(
@@ -113,6 +122,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
113
122
  lifecycle.installSignalHandlers();
114
123
  let results = [];
115
124
  let finishedAt = Date.now();
125
+ let knownFailureIssueValidation = null;
116
126
 
117
127
  try {
118
128
  if (executedPlans.length > 0) {
@@ -168,7 +178,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
168
178
  results = configs.map((config) =>
169
179
  finalizeServiceResult(trackers.get(config.name), startedAt, finishedAt)
170
180
  );
171
- const artifact = buildRunArtifact({
181
+ const runArtifact = buildRunArtifact({
172
182
  productDir,
173
183
  results,
174
184
  startedAt,
@@ -185,12 +195,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
185
195
  metadata,
186
196
  summarizeDbBackend,
187
197
  });
188
-
189
- writeRunArtifact(productDir, artifact);
190
- if (opts.writeStatus) {
191
- writeStatusArtifact(
192
- productDir,
193
- buildStatusArtifact({
198
+ const statusArtifact = opts.writeStatus
199
+ ? buildStatusArtifact({
194
200
  productDir,
195
201
  results,
196
202
  typeValues,
@@ -200,12 +206,37 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
200
206
  serviceFilter: opts.serviceFilter || null,
201
207
  metadata,
202
208
  })
203
- );
209
+ : null;
210
+ const enrichedArtifacts = applyKnownFailuresToArtifacts(
211
+ runArtifact,
212
+ statusArtifact,
213
+ knownFailures
214
+ );
215
+ knownFailureIssueValidation = await validateKnownFailureIssues({
216
+ productDir,
217
+ document: knownFailures,
218
+ runArtifact: enrichedArtifacts.runArtifact,
219
+ statusArtifact: enrichedArtifacts.statusArtifact,
220
+ config: configs[0]?.testkit?.reporting?.issueValidation || null,
221
+ gitMetadata: metadata.git,
222
+ });
223
+ attachKnownFailureIssueValidation(
224
+ enrichedArtifacts.runArtifact,
225
+ enrichedArtifacts.statusArtifact,
226
+ knownFailureIssueValidation
227
+ );
228
+
229
+ writeRunArtifact(productDir, enrichedArtifacts.runArtifact);
230
+ if (opts.writeStatus) {
231
+ writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
204
232
  }
205
233
 
206
- printRunSummary(results, finishedAt - startedAt);
207
- await reportTelemetry(telemetry, artifact);
234
+ printRunSummary(results, finishedAt - startedAt, knownFailureIssueValidation);
235
+ await reportTelemetry(telemetry, enrichedArtifacts.runArtifact);
208
236
  if (results.some((result) => result.failed)) exitCode = 1;
237
+ if (shouldFailKnownFailureIssueValidation(knownFailureIssueValidation)) {
238
+ exitCode = 1;
239
+ }
209
240
  if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
210
241
  } finally {
211
242
  if (lifecycle.isStopRequested()) {
@@ -259,8 +290,8 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
259
290
  });
260
291
  }
261
292
 
262
- function printRunSummary(results, durationMs) {
263
- for (const line of buildRunSummaryLines(results, durationMs)) {
293
+ function printRunSummary(results, durationMs, knownFailureIssueValidation = null) {
294
+ for (const line of buildRunSummaryLines(results, durationMs, knownFailureIssueValidation)) {
264
295
  console.log(line);
265
296
  }
266
297
  }
@@ -289,3 +320,11 @@ async function reportTelemetry(telemetry, artifact) {
289
320
  function normalizePathSeparators(filePath) {
290
321
  return filePath.split("\\").join("/");
291
322
  }
323
+
324
+ function attachKnownFailureIssueValidation(runArtifact, statusArtifact, validation) {
325
+ if (!validation) return;
326
+ runArtifact.knownFailuresIssueValidation = validation;
327
+ if (statusArtifact) {
328
+ statusArtifact.knownFailuresIssueValidation = validation;
329
+ }
330
+ }
@@ -76,5 +76,6 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
76
76
  durationMs: fileResult?.durationMs > 0 ? fileResult.durationMs : durationMs,
77
77
  startedAt,
78
78
  finishedAt,
79
+ failureDetails: timedOut ? [] : fileResult?.failureDetails || [],
79
80
  };
80
81
  }
@@ -23,9 +23,15 @@ export function buildStatusArtifact({
23
23
  path: file.path,
24
24
  status: file.status,
25
25
  };
26
+ if (file.error) {
27
+ test.error = file.error;
28
+ }
26
29
  if (file.reason) {
27
30
  test.reason = file.reason;
28
31
  }
32
+ if (Array.isArray(file.failureDetails) && file.failureDetails.length > 0) {
33
+ test.failureDetails = file.failureDetails;
34
+ }
29
35
  tests.push(test);
30
36
  }
31
37
  }
@@ -72,7 +78,7 @@ export function buildStatusArtifact({
72
78
  scope.serviceFilter === null;
73
79
 
74
80
  return {
75
- schemaVersion: 3,
81
+ schemaVersion: 5,
76
82
  source: "testkit",
77
83
  notice: "Generated file. Do not edit manually.",
78
84
  product: {
@@ -121,7 +127,7 @@ export function buildRunArtifact({
121
127
  const dbBackend = summarizeDbBackend(results);
122
128
 
123
129
  return {
124
- schemaVersion: 3,
130
+ schemaVersion: 5,
125
131
  source: "testkit",
126
132
  generatedAt: new Date(finishedAt).toISOString(),
127
133
  product: {
@@ -78,7 +78,7 @@ describe("runner reporting", () => {
78
78
  });
79
79
 
80
80
  expect(artifact.product.name).toBe("my-product");
81
- expect(artifact.schemaVersion).toBe(3);
81
+ expect(artifact.schemaVersion).toBe(5);
82
82
  expect(artifact.run).toMatchObject({
83
83
  workers: 2,
84
84
  fileTimeoutSeconds: 60,
@@ -149,7 +149,7 @@ describe("runner reporting", () => {
149
149
  });
150
150
 
151
151
  expect(status).toEqual({
152
- schemaVersion: 3,
152
+ schemaVersion: 5,
153
153
  source: "testkit",
154
154
  notice: "Generated file. Do not edit manually.",
155
155
  product: {
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { mergeFailureDetails } from "./failure-details.mjs";
2
3
 
3
4
  export function buildServiceTrackers(servicePlans, startedAt) {
4
5
  const trackers = new Map();
@@ -46,6 +47,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
46
47
  reason: null,
47
48
  status: "not_run",
48
49
  artifacts: [],
50
+ failureDetails: [],
49
51
  },
50
52
  ];
51
53
  }),
@@ -59,6 +61,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
59
61
  reason: file.reason,
60
62
  status: "skipped",
61
63
  artifacts: [],
64
+ failureDetails: [],
62
65
  },
63
66
  ]),
64
67
  ]),
@@ -121,6 +124,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
121
124
  existingFileResult.reason = outcome.reason || null;
122
125
  existingFileResult.status = status;
123
126
  existingFileResult.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : [];
127
+ existingFileResult.failureDetails = mergeFailureDetails(outcome.failureDetails);
124
128
  } else {
125
129
  suite.fileResultsByPath.set(normalizedPath, {
126
130
  path: normalizedPath,
@@ -130,6 +134,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
130
134
  reason: outcome.reason || null,
131
135
  status,
132
136
  artifacts: Array.isArray(outcome.artifacts) ? outcome.artifacts : [],
137
+ failureDetails: mergeFailureDetails(outcome.failureDetails),
133
138
  });
134
139
  }
135
140
  if (status === "failed" && !suite.failedFileSet.has(task.file)) {
@@ -247,6 +252,9 @@ function finalizeSuite(suite) {
247
252
  durationMs: file.durationMs,
248
253
  error: file.error,
249
254
  reason: file.reason,
255
+ ...(Array.isArray(file.failureDetails) && file.failureDetails.length > 0
256
+ ? { failureDetails: file.failureDetails }
257
+ : {}),
250
258
  ...(Array.isArray(file.artifacts) && file.artifacts.length > 0
251
259
  ? { artifacts: file.artifacts }
252
260
  : {}),
@@ -0,0 +1,154 @@
1
+ import {
2
+ buildKnownFailureFileIdentity,
3
+ findMatchingKnownFailureEntries,
4
+ loadKnownFailuresConfig,
5
+ } from "../known-failures/index.mjs";
6
+
7
+ export { loadKnownFailuresConfig };
8
+
9
+ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, knownFailures) {
10
+ if (!knownFailures) return { runArtifact, statusArtifact };
11
+
12
+ const runEntries = extractRunFileEntries(runArtifact);
13
+ const statusEntries = extractStatusFileEntries(statusArtifact);
14
+ const fileSummaries = new Map();
15
+
16
+ for (const entry of [...runEntries, ...statusEntries]) {
17
+ const key = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
18
+ if (!fileSummaries.has(key)) {
19
+ fileSummaries.set(key, {
20
+ service: entry.service,
21
+ type: entry.type,
22
+ path: entry.path,
23
+ status: entry.status,
24
+ error: entry.error || null,
25
+ failureDetails: Array.isArray(entry.failureDetails) ? entry.failureDetails : [],
26
+ });
27
+ }
28
+ }
29
+
30
+ const matchesByFileKey = new Map();
31
+ const matchedByFailedEntryIds = new Set();
32
+
33
+ for (const fileSummary of fileSummaries.values()) {
34
+ const matches = findMatchingKnownFailureEntries(knownFailures, fileSummary);
35
+ if (matches.length === 0) continue;
36
+
37
+ const fileKey = buildKnownFailureFileIdentity(
38
+ fileSummary.service,
39
+ fileSummary.type,
40
+ fileSummary.path
41
+ );
42
+ matchesByFileKey.set(fileKey, matches.map((entry) => toArtifactTriageEntry(entry)));
43
+ if (fileSummary.status === "failed") {
44
+ for (const entry of matches) {
45
+ matchedByFailedEntryIds.add(entry.id);
46
+ }
47
+ }
48
+ }
49
+
50
+ for (const entry of [...runEntries, ...statusEntries]) {
51
+ const fileKey = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
52
+ const matches = matchesByFileKey.get(fileKey) || [];
53
+ if (matches.length === 0) {
54
+ if (entry.status === "failed") {
55
+ setEntryTriage(entry, {
56
+ status: "untriaged",
57
+ entries: [],
58
+ });
59
+ }
60
+ continue;
61
+ }
62
+
63
+ setEntryTriage(entry, {
64
+ status: entry.status === "failed" ? "known_failure" : "known_issue_not_reproduced",
65
+ classifications: [...new Set(matches.map((match) => match.classification))].sort(),
66
+ entries: matches,
67
+ });
68
+ }
69
+
70
+ const summaryTests = statusArtifact?.tests || runEntries;
71
+ const triageSummary = buildTriageSummary(
72
+ summaryTests,
73
+ knownFailures.entries,
74
+ matchedByFailedEntryIds
75
+ );
76
+ runArtifact.triageSummary = triageSummary;
77
+ if (statusArtifact) {
78
+ statusArtifact.triageSummary = triageSummary;
79
+ }
80
+ return { runArtifact, statusArtifact };
81
+ }
82
+
83
+ function toArtifactTriageEntry(entry) {
84
+ return {
85
+ id: entry.id,
86
+ title: entry.title,
87
+ classification: entry.classification,
88
+ state: entry.state,
89
+ issue: entry.issue,
90
+ description: entry.description,
91
+ whyFailing: entry.whyFailing,
92
+ lastReviewedAt: entry.lastReviewedAt,
93
+ };
94
+ }
95
+
96
+ function buildTriageSummary(tests, entries, matchedEntryIds) {
97
+ const failedTests = tests.filter((test) => test.status === "failed");
98
+ const knownFailedTests = failedTests.filter((test) => test.triage?.status === "known_failure");
99
+ const byClassification = {};
100
+
101
+ for (const test of knownFailedTests) {
102
+ for (const classification of test.triage?.classifications || []) {
103
+ byClassification[classification] = (byClassification[classification] || 0) + 1;
104
+ }
105
+ }
106
+
107
+ return {
108
+ failed: {
109
+ total: failedTests.length,
110
+ known: knownFailedTests.length,
111
+ untriaged: failedTests.length - knownFailedTests.length,
112
+ byClassification,
113
+ },
114
+ entries: {
115
+ total: entries.length,
116
+ matchedByFailedTests: matchedEntryIds.size,
117
+ unmatched: entries.length - matchedEntryIds.size,
118
+ },
119
+ };
120
+ }
121
+
122
+ function extractRunFileEntries(runArtifact) {
123
+ const entries = [];
124
+
125
+ for (const service of runArtifact.services || []) {
126
+ for (const suite of service.suites || []) {
127
+ for (const file of suite.files || []) {
128
+ entries.push({
129
+ target: file,
130
+ service: service.name,
131
+ type: suite.type,
132
+ path: file.path,
133
+ status: file.status,
134
+ error: file.error || null,
135
+ failureDetails: Array.isArray(file.failureDetails) ? file.failureDetails : [],
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ return entries;
142
+ }
143
+
144
+ function extractStatusFileEntries(statusArtifact) {
145
+ return statusArtifact?.tests || [];
146
+ }
147
+
148
+ function setEntryTriage(entry, triage) {
149
+ if (entry?.target) {
150
+ entry.target.triage = triage;
151
+ return;
152
+ }
153
+ entry.triage = triage;
154
+ }