@elench/testkit 0.1.53 → 0.1.55

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 (41) hide show
  1. package/lib/cli/commands/artifacts.mjs +2 -2
  2. package/lib/cli/commands/logs.mjs +2 -2
  3. package/lib/cli/commands/show.mjs +2 -2
  4. package/lib/cli/db.mjs +17 -2
  5. package/lib/cli/presentation/code-frames.mjs +57 -0
  6. package/lib/cli/presentation/code-frames.test.mjs +71 -0
  7. package/lib/cli/presentation/colors.mjs +29 -0
  8. package/lib/cli/presentation/run-reporter.mjs +41 -7
  9. package/lib/cli/presentation/run-reporter.test.mjs +80 -0
  10. package/lib/cli/tui/watch-app.mjs +134 -18
  11. package/lib/cli/viewer.mjs +146 -4
  12. package/lib/database/index.mjs +85 -11
  13. package/lib/database/template-steps.mjs +45 -6
  14. package/lib/database/template-steps.test.mjs +43 -0
  15. package/lib/known-failures/index.mjs +1 -1
  16. package/lib/known-failures/index.test.mjs +46 -0
  17. package/lib/runner/artifacts.mjs +16 -0
  18. package/lib/runner/default-runtime-errors.mjs +66 -0
  19. package/lib/runner/default-runtime-runner.mjs +8 -1
  20. package/lib/runner/failure-details.mjs +31 -0
  21. package/lib/runner/failure-details.test.mjs +51 -0
  22. package/lib/runner/formatting.mjs +114 -4
  23. package/lib/runner/formatting.test.mjs +77 -0
  24. package/lib/runner/logs.mjs +71 -6
  25. package/lib/runner/orchestrator.mjs +63 -7
  26. package/lib/runner/reporting.mjs +52 -2
  27. package/lib/runner/reporting.test.mjs +80 -2
  28. package/lib/runner/runtime-contexts.mjs +3 -3
  29. package/lib/runner/runtime-preparation.mjs +31 -0
  30. package/lib/runner/setup-operations.mjs +115 -0
  31. package/lib/runner/setup-operations.test.mjs +94 -0
  32. package/lib/runner/template-steps.mjs +129 -11
  33. package/lib/runner/triage.mjs +67 -0
  34. package/lib/runtime/index.d.ts +60 -0
  35. package/lib/runtime/index.mjs +12 -0
  36. package/lib/runtime-src/k6/checks.js +45 -12
  37. package/lib/runtime-src/k6/http-assertions.js +214 -0
  38. package/lib/runtime-src/k6/http.js +261 -13
  39. package/lib/runtime-src/k6/suite.js +46 -1
  40. package/lib/toolchains/index.mjs +0 -4
  41. package/package.json +3 -1
@@ -8,7 +8,10 @@ import {
8
8
  formatFileTimeoutBudgetError,
9
9
  } from "../shared/file-timeout.mjs";
10
10
  import { persistTaskArtifacts, persistTaskOutputArtifacts } from "./artifacts.mjs";
11
- import { determineDefaultRuntimeFailure } from "./default-runtime-errors.mjs";
11
+ import {
12
+ determineDefaultRuntimeFailure,
13
+ extractDefaultRuntimeFatalDetail,
14
+ } from "./default-runtime-errors.mjs";
12
15
  import { collectFailureDetailsFromRuntimeArtifacts } from "./failure-details.mjs";
13
16
  import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
14
17
  import { readDatabaseUrl } from "./state-io.mjs";
@@ -147,6 +150,10 @@ export async function runDefaultRuntimeTask(
147
150
  : null,
148
151
  ]);
149
152
  const failureDetails = collectFailureDetailsFromRuntimeArtifacts(rawRuntimeArtifacts);
153
+ const fatalRuntimeDetail = extractDefaultRuntimeFatalDetail(result.stderr || "", getFirstLine);
154
+ if (fatalRuntimeDetail && !failureDetails.some((detail) => detail?.kind === "runtime-exception")) {
155
+ failureDetails.unshift(fatalRuntimeDetail);
156
+ }
150
157
  const runtimeError = timedOut
151
158
  ? formatFileTimeoutBudgetError(fileTimeoutSeconds)
152
159
  : determineDefaultRuntimeFailure(result, summary, getFirstLine);
@@ -28,6 +28,24 @@ export function normalizeFailureDetail(detail) {
28
28
  const message = normalizeNonEmptyString(detail.message);
29
29
  if (message) normalized.message = message;
30
30
 
31
+ if (detail.expected !== undefined) normalized.expected = cloneJsonValue(detail.expected);
32
+ if (detail.actual !== undefined) normalized.actual = cloneJsonValue(detail.actual);
33
+
34
+ const traceId = normalizeNonEmptyString(detail.traceId);
35
+ if (traceId) normalized.traceId = traceId;
36
+
37
+ const request = normalizeRecord(detail.request);
38
+ if (request) normalized.request = request;
39
+
40
+ const response = normalizeRecord(detail.response);
41
+ if (response) normalized.response = response;
42
+
43
+ const location = normalizeRecord(detail.location);
44
+ if (location) normalized.location = location;
45
+
46
+ const stack = normalizeNonEmptyString(detail.stack);
47
+ if (stack) normalized.stack = stack;
48
+
31
49
  return normalized;
32
50
  }
33
51
 
@@ -89,3 +107,16 @@ function normalizePositiveInteger(value) {
89
107
  if (!Number.isInteger(value) || value <= 0) return null;
90
108
  return value;
91
109
  }
110
+
111
+ function normalizeRecord(value) {
112
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
113
+ return cloneJsonValue(value);
114
+ }
115
+
116
+ function cloneJsonValue(value) {
117
+ try {
118
+ return JSON.parse(JSON.stringify(value));
119
+ } catch {
120
+ return String(value);
121
+ }
122
+ }
@@ -60,4 +60,55 @@ describe("runner failure details", () => {
60
60
  },
61
61
  ]);
62
62
  });
63
+
64
+ it("preserves rich assertion metadata", () => {
65
+ expect(
66
+ mergeFailureDetails([
67
+ {
68
+ kind: "http-assertion",
69
+ key: "GET /health > status is 200",
70
+ title: "status is 200",
71
+ expected: 200,
72
+ actual: 404,
73
+ request: {
74
+ method: "GET",
75
+ path: "/health",
76
+ requestId: "req-1",
77
+ },
78
+ response: {
79
+ status: 404,
80
+ bodyPreview: '{"error":"nope"}',
81
+ },
82
+ location: {
83
+ path: "/tmp/example.ts",
84
+ line: 12,
85
+ column: 4,
86
+ },
87
+ },
88
+ ])
89
+ ).toEqual([
90
+ {
91
+ kind: "http-assertion",
92
+ key: "GET /health > status is 200",
93
+ title: "status is 200",
94
+ count: 1,
95
+ expected: 200,
96
+ actual: 404,
97
+ request: {
98
+ method: "GET",
99
+ path: "/health",
100
+ requestId: "req-1",
101
+ },
102
+ response: {
103
+ status: 404,
104
+ bodyPreview: '{"error":"nope"}',
105
+ },
106
+ location: {
107
+ path: "/tmp/example.ts",
108
+ line: 12,
109
+ column: 4,
110
+ },
111
+ },
112
+ ]);
113
+ });
63
114
  });
@@ -69,7 +69,7 @@ export function buildCompactRunSummaryLines(
69
69
  for (const failure of failures) {
70
70
  lines.push(` ${failure.file.path}`);
71
71
  lines.push(` ${failure.primaryMessage}`);
72
- for (const detail of failure.extraDetails.slice(0, 2)) {
72
+ for (const detail of failure.extraLines.slice(0, 3)) {
73
73
  lines.push(` ${detail}`);
74
74
  }
75
75
  }
@@ -209,14 +209,33 @@ function collectFailedFiles(results) {
209
209
  for (const suite of result.suites || []) {
210
210
  for (const file of suite.files || []) {
211
211
  if (file.status !== "failed") continue;
212
- const detailMessages = (file.failureDetails || [])
212
+ const rankedDetails = rankFailureDetails(file.failureDetails || []);
213
+ const primaryDetail = rankedDetails[0] || null;
214
+ const fallbackMessages = rankedDetails
213
215
  .map((detail) => detail.message || detail.title)
214
216
  .filter(Boolean)
215
217
  .map((message) => sanitizeErrorMessage(String(message).trim()));
218
+ const extraLines = [];
219
+ if (primaryDetail) {
220
+ const responseLine = formatFailureResponsePreview(primaryDetail);
221
+ if (responseLine) extraLines.push(responseLine);
222
+ const triageLine = formatTriageLine(file.triage || null);
223
+ if (triageLine) extraLines.push(triageLine);
224
+ const requestLine = formatFailureRequestHint(primaryDetail);
225
+ if (requestLine) extraLines.push(requestLine);
226
+ } else {
227
+ const triageLine = formatTriageLine(file.triage || null);
228
+ if (triageLine) extraLines.push(triageLine);
229
+ }
230
+
231
+ for (const detail of rankedDetails.slice(primaryDetail ? 1 : 0)) {
232
+ const line = sanitizeErrorMessage(String(detail.message || detail.title || "").trim());
233
+ if (line && !extraLines.includes(line)) extraLines.push(line);
234
+ }
216
235
  failures.push({
217
236
  file,
218
- primaryMessage: sanitizeErrorMessage(file.error || detailMessages[0] || suite.error || "Failed"),
219
- extraDetails: detailMessages.slice(file.error ? 0 : 1),
237
+ primaryMessage: resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages),
238
+ extraLines,
220
239
  });
221
240
  }
222
241
  }
@@ -224,6 +243,97 @@ function collectFailedFiles(results) {
224
243
  return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
225
244
  }
226
245
 
246
+ function resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages) {
247
+ if (primaryDetail?.message) {
248
+ return sanitizeErrorMessage(String(primaryDetail.message).trim());
249
+ }
250
+
251
+ const fileError = sanitizeErrorMessage(file.error || "");
252
+ if (fileError && !isThresholdWrapperMessage(fileError)) {
253
+ return fileError;
254
+ }
255
+
256
+ if (fallbackMessages.length > 0) {
257
+ return fallbackMessages[0];
258
+ }
259
+
260
+ if (fileError) {
261
+ return fileError;
262
+ }
263
+
264
+ return sanitizeErrorMessage(suite.error || "Failed");
265
+ }
266
+
267
+ function rankFailureDetails(details) {
268
+ return [...(Array.isArray(details) ? details : [])].sort((left, right) => {
269
+ return failureDetailRank(left) - failureDetailRank(right) || String(left?.key || "").localeCompare(String(right?.key || ""));
270
+ });
271
+ }
272
+
273
+ function failureDetailRank(detail) {
274
+ if (detail?.kind === "http-assertion") return 1;
275
+ if (detail?.request && detail?.response) return 2;
276
+ if (detail?.location || detail?.stack) return 3;
277
+ if (detail?.message) return 4;
278
+ return 5;
279
+ }
280
+
281
+ function formatFailureResponsePreview(detail) {
282
+ const bodyPreview = detail?.response?.bodyPreview;
283
+ if (!bodyPreview) return null;
284
+ return `response: ${sanitizeInline(bodyPreview, 220)}`;
285
+ }
286
+
287
+ function formatFailureRequestHint(detail) {
288
+ const method = detail?.request?.method;
289
+ const path = detail?.request?.path;
290
+ if (!method || !path) return null;
291
+ const requestId = detail?.request?.requestId;
292
+ if (requestId) {
293
+ return `logs: requestId=${requestId}`;
294
+ }
295
+ return `request: ${method} ${path}`;
296
+ }
297
+
298
+ function formatTriageLine(triage) {
299
+ if (!triage) return null;
300
+ if (triage.status === "untriaged") return "triage: untriaged";
301
+
302
+ const entry = triage.entries?.[0];
303
+ if (!entry?.issue) {
304
+ if (triage.status === "validation_unavailable") {
305
+ const reason = triage.availability?.reason ? ` (${triage.availability.reason})` : "";
306
+ return `triage: validation unavailable${reason}`;
307
+ }
308
+ return null;
309
+ }
310
+
311
+ const issueLabel = `#${entry.issue.number}`;
312
+ const validationStatus = entry.validationStatus || null;
313
+ if (validationStatus === "closed_but_failing") {
314
+ return `triage: known issue ${issueLabel} closed but still failing`;
315
+ }
316
+ if (validationStatus === "validation_unavailable") {
317
+ const reason = triage.availability?.mode === "cache" ? "cache" : "validation unavailable";
318
+ return `triage: known issue ${issueLabel} (${reason})`;
319
+ }
320
+ if (entry.github?.state === "open" || entry.state === "open") {
321
+ return `triage: known issue ${issueLabel} open`;
322
+ }
323
+ if (entry.github?.state === "closed" || entry.state === "closed") {
324
+ return `triage: known issue ${issueLabel} closed`;
325
+ }
326
+ return `triage: known issue ${issueLabel}`;
327
+ }
328
+
329
+ function isThresholdWrapperMessage(message) {
330
+ return /Default runtime thresholds failed:/.test(String(message || ""));
331
+ }
332
+
333
+ function sanitizeInline(message, maxLength = 180) {
334
+ return String(message || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
335
+ }
336
+
227
337
  function collectServiceErrors(results) {
228
338
  const items = [];
229
339
  for (const result of results) {
@@ -162,4 +162,81 @@ describe("runner formatting", () => {
162
162
  expect(lines.join("\n")).toContain("2 closed issues still failing");
163
163
  expect(lines.join("\n")).toContain("1 title mismatch");
164
164
  });
165
+
166
+ it("prefers structured HTTP assertion details over threshold wrapper text", () => {
167
+ const lines = buildRunSummaryLines(
168
+ [
169
+ {
170
+ name: "api",
171
+ skipped: false,
172
+ failed: true,
173
+ suiteCount: 1,
174
+ completedSuiteCount: 1,
175
+ skippedSuiteCount: 0,
176
+ failedSuiteCount: 1,
177
+ totalFileCount: 1,
178
+ passedFileCount: 0,
179
+ failedFileCount: 1,
180
+ skippedFileCount: 0,
181
+ notRunFileCount: 0,
182
+ durationMs: 500,
183
+ suites: [
184
+ {
185
+ failed: true,
186
+ type: "int",
187
+ name: "default",
188
+ framework: "k6",
189
+ failedFiles: ["__testkit__/health/health.int.testkit.ts"],
190
+ durationMs: 500,
191
+ files: [
192
+ {
193
+ path: "__testkit__/health/health.int.testkit.ts",
194
+ status: "failed",
195
+ error: "Default runtime thresholds failed: checks(rate==1.0)",
196
+ failureDetails: [
197
+ {
198
+ kind: "http-assertion",
199
+ key: "GET /health > status is 200",
200
+ title: "status is 200",
201
+ message: "GET /health expected 200, got 404",
202
+ request: {
203
+ method: "GET",
204
+ path: "/health",
205
+ requestId: "req-1",
206
+ },
207
+ response: {
208
+ status: 404,
209
+ bodyPreview: '{"error":"nope"}',
210
+ },
211
+ },
212
+ ],
213
+ triage: {
214
+ status: "known_failure",
215
+ entries: [
216
+ {
217
+ id: "health-is-bad",
218
+ state: "open",
219
+ issue: {
220
+ repo: "acme/example",
221
+ number: 42,
222
+ },
223
+ },
224
+ ],
225
+ },
226
+ },
227
+ ],
228
+ error: null,
229
+ },
230
+ ],
231
+ errors: [],
232
+ },
233
+ ],
234
+ 500
235
+ );
236
+
237
+ expect(lines.join("\n")).toContain("GET /health expected 200, got 404");
238
+ expect(lines.join("\n")).toContain('response: {"error":"nope"}');
239
+ expect(lines.join("\n")).toContain("triage: known issue #42 open");
240
+ expect(lines.join("\n")).not.toContain("Default runtime thresholds failed: checks(rate==1.0)\n response:");
241
+ });
165
242
  });
@@ -2,21 +2,26 @@ import fs from "fs";
2
2
  import path from "path";
3
3
 
4
4
  const RESULT_LOGS_DIRNAME = "logs";
5
+ const RESULT_SETUP_DIRNAME = "setup";
5
6
 
6
7
  export function createRunLogRegistry(productDir) {
7
- const records = new Map();
8
+ const serviceRecords = new Map();
9
+ const setupRecords = new Map();
8
10
 
9
11
  return {
10
12
  ensureServiceLogRecord(config) {
11
13
  const key = `${config.runtimeLabel || config.name}:${config.name}`;
12
- const existing = records.get(key);
14
+ const existing = serviceRecords.get(key);
13
15
  if (existing) return existing;
14
16
 
15
17
  const fileName = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}.log`;
16
18
  const relativePath = path.join(".testkit", "results", RESULT_LOGS_DIRNAME, fileName);
17
19
  const absolutePath = path.join(productDir, relativePath);
18
20
  fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
19
- const stream = fs.createWriteStream(absolutePath, { flags: "a" });
21
+ const stream = fs.createWriteStream(absolutePath, {
22
+ fd: fs.openSync(absolutePath, "a"),
23
+ flags: "a",
24
+ });
20
25
  const record = {
21
26
  key,
22
27
  serviceName: config.name,
@@ -25,7 +30,32 @@ export function createRunLogRegistry(productDir) {
25
30
  absolutePath,
26
31
  stream,
27
32
  };
28
- records.set(key, record);
33
+ serviceRecords.set(key, record);
34
+ return record;
35
+ },
36
+ ensureSetupLogRecord(config, stage) {
37
+ const key = `${config.runtimeLabel || config.name}:${config.name}:${stage}`;
38
+ const existing = setupRecords.get(key);
39
+ if (existing) return existing;
40
+
41
+ const fileName = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}__${sanitizePathSegment(stage)}.log`;
42
+ const relativePath = path.join(".testkit", "results", RESULT_SETUP_DIRNAME, fileName);
43
+ const absolutePath = path.join(productDir, relativePath);
44
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
45
+ const stream = fs.createWriteStream(absolutePath, {
46
+ fd: fs.openSync(absolutePath, "a"),
47
+ flags: "a",
48
+ });
49
+ const record = {
50
+ key,
51
+ serviceName: config.name,
52
+ runtimeLabel: config.runtimeLabel || config.name,
53
+ stage,
54
+ path: normalizePath(relativePath),
55
+ absolutePath,
56
+ stream,
57
+ };
58
+ setupRecords.set(key, record);
29
59
  return record;
30
60
  },
31
61
  append(record, streamName, line) {
@@ -33,7 +63,7 @@ export function createRunLogRegistry(productDir) {
33
63
  record.stream.write(`${new Date().toISOString()} [${streamName}] ${line}\n`);
34
64
  },
35
65
  listServiceLogs() {
36
- return [...records.values()]
66
+ return [...serviceRecords.values()]
37
67
  .map((record) => ({
38
68
  serviceName: record.serviceName,
39
69
  runtimeLabel: record.runtimeLabel,
@@ -45,8 +75,26 @@ export function createRunLogRegistry(productDir) {
45
75
  left.runtimeLabel.localeCompare(right.runtimeLabel)
46
76
  );
47
77
  },
78
+ listSetupLogs() {
79
+ return [...setupRecords.values()]
80
+ .map((record) => ({
81
+ serviceName: record.serviceName,
82
+ runtimeLabel: record.runtimeLabel,
83
+ stage: record.stage,
84
+ path: record.path,
85
+ }))
86
+ .sort(
87
+ (left, right) =>
88
+ left.serviceName.localeCompare(right.serviceName) ||
89
+ left.runtimeLabel.localeCompare(right.runtimeLabel) ||
90
+ left.stage.localeCompare(right.stage)
91
+ );
92
+ },
48
93
  closeAll() {
49
- for (const record of records.values()) {
94
+ for (const record of serviceRecords.values()) {
95
+ record.stream.end();
96
+ }
97
+ for (const record of setupRecords.values()) {
50
98
  record.stream.end();
51
99
  }
52
100
  },
@@ -59,6 +107,23 @@ export function readLogTail(absolutePath, lineCount = 80) {
59
107
  return lines.slice(Math.max(0, lines.length - lineCount));
60
108
  }
61
109
 
110
+ export function findLogSliceByRequestId(absolutePath, requestId, contextLines = 2) {
111
+ if (!absolutePath || !requestId || !fs.existsSync(absolutePath)) return [];
112
+ const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/).filter(Boolean);
113
+ const matches = [];
114
+
115
+ for (let index = 0; index < lines.length; index += 1) {
116
+ if (!lines[index].includes(requestId)) continue;
117
+ const start = Math.max(0, index - contextLines);
118
+ const end = Math.min(lines.length, index + contextLines + 1);
119
+ for (let cursor = start; cursor < end; cursor += 1) {
120
+ if (!matches.includes(lines[cursor])) matches.push(lines[cursor]);
121
+ }
122
+ }
123
+
124
+ return matches;
125
+ }
126
+
62
127
  function sanitizePathSegment(value) {
63
128
  return String(value)
64
129
  .trim()
@@ -14,8 +14,12 @@ import {
14
14
  recordTaskOutcome,
15
15
  summarizeDbBackend,
16
16
  } from "./results.mjs";
17
- import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
18
- import { applyKnownFailuresToArtifacts, loadKnownFailuresConfig } from "./triage.mjs";
17
+ import { buildLiveRunArtifact, buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
18
+ import {
19
+ applyKnownFailureIssueValidationToArtifacts,
20
+ applyKnownFailuresToArtifacts,
21
+ loadKnownFailuresConfig,
22
+ } from "./triage.mjs";
19
23
  import { formatError } from "./formatting.mjs";
20
24
  import {
21
25
  shouldFailKnownFailureIssueValidation,
@@ -25,10 +29,12 @@ import {
25
29
  loadTimings,
26
30
  resetResultArtifacts,
27
31
  saveTimings,
32
+ writeLiveRunArtifact,
28
33
  writeRunArtifact,
29
34
  writeStatusArtifact,
30
35
  } from "./artifacts.mjs";
31
36
  import { createRunLogRegistry } from "./logs.mjs";
37
+ import { createSetupOperationRegistry } from "./setup-operations.mjs";
32
38
  import {
33
39
  cleanupRunById,
34
40
  cleanupRuns,
@@ -68,6 +74,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
68
74
  );
69
75
  const reporter = opts.reporter || null;
70
76
  const logRegistry = createRunLogRegistry(productDir);
77
+ let workerCount = 0;
78
+ let runtimeInstanceCount = 0;
79
+ let runtimeStats = [];
71
80
  const requestedFiles = opts.fileNames || [];
72
81
  if (requestedFiles.length > 0) {
73
82
  const unmatchedFiles = findUnmatchedRequestedFiles(
@@ -116,10 +125,40 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
116
125
  reporter
117
126
  );
118
127
  const trackers = buildServiceTrackers(servicePlans, startedAt);
128
+ const writeLiveSnapshot = () => {
129
+ const now = Date.now();
130
+ const partialResults = configs.map((config) =>
131
+ finalizeServiceResult(trackers.get(config.name), startedAt, now)
132
+ );
133
+ writeLiveRunArtifact(
134
+ productDir,
135
+ buildLiveRunArtifact({
136
+ productDir,
137
+ results: partialResults,
138
+ startedAt,
139
+ updatedAt: now,
140
+ execution,
141
+ workerCount,
142
+ runtimeInstanceCount,
143
+ runtimeStats,
144
+ typeValues,
145
+ suiteSelectors,
146
+ fileNames: requestedFiles,
147
+ shard: opts.shard || null,
148
+ serviceFilter: opts.serviceFilter || null,
149
+ metadata,
150
+ summarizeDbBackend,
151
+ serviceLogs: logRegistry.listServiceLogs(),
152
+ setupLogs: logRegistry.listSetupLogs(),
153
+ setupOperations: setupRegistry.listOperations(),
154
+ })
155
+ );
156
+ };
157
+ const setupRegistry = createSetupOperationRegistry({
158
+ logRegistry,
159
+ onChange: writeLiveSnapshot,
160
+ });
119
161
  const executedPlans = servicePlans.filter((plan) => !plan.skipped);
120
- let workerCount = 0;
121
- let runtimeInstanceCount = 0;
122
- let runtimeStats = [];
123
162
  let exitCode = 0;
124
163
  const lifecycle = createRunLifecycle(productDir);
125
164
  lifecycle.markRunning();
@@ -127,6 +166,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
127
166
  let results = [];
128
167
  let finishedAt = Date.now();
129
168
  let knownFailureIssueValidation = null;
169
+ writeLiveSnapshot();
130
170
 
131
171
  try {
132
172
  if (executedPlans.length > 0) {
@@ -145,6 +185,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
145
185
  runtimeOptions: {
146
186
  reporter,
147
187
  logRegistry,
188
+ setupRegistry,
148
189
  },
149
190
  });
150
191
  const timingUpdates = [];
@@ -160,8 +201,14 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
160
201
  timingUpdates,
161
202
  lifecycle,
162
203
  claimNextTask,
163
- recordTaskOutcome,
164
- recordGraphError,
204
+ (allTrackers, task, outcome, now) => {
205
+ recordTaskOutcome(allTrackers, task, outcome, now);
206
+ writeLiveSnapshot();
207
+ },
208
+ (allTrackers, graph, message, now) => {
209
+ recordGraphError(allTrackers, graph, message, now);
210
+ writeLiveSnapshot();
211
+ },
165
212
  reporter
166
213
  )
167
214
  )
@@ -173,9 +220,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
173
220
  for (const tracker of trackers.values()) {
174
221
  if (!tracker.skipped) addTrackerError(tracker, message);
175
222
  }
223
+ writeLiveSnapshot();
176
224
  }
177
225
  }
178
226
  runtimeStats = runtimeManager.getStats();
227
+ writeLiveSnapshot();
179
228
  } finally {
180
229
  await runtimeManager.cleanupAll();
181
230
  }
@@ -204,6 +253,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
204
253
  metadata,
205
254
  summarizeDbBackend,
206
255
  serviceLogs: logRegistry.listServiceLogs(),
256
+ setupLogs: logRegistry.listSetupLogs(),
257
+ setupOperations: setupRegistry.listOperations(),
207
258
  });
208
259
  const statusArtifact = opts.writeStatus
209
260
  ? buildStatusArtifact({
@@ -230,6 +281,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
230
281
  config: configs[0]?.testkit?.reporting?.issueValidation || null,
231
282
  gitMetadata: metadata.git,
232
283
  });
284
+ applyKnownFailureIssueValidationToArtifacts(
285
+ enrichedArtifacts.runArtifact,
286
+ enrichedArtifacts.statusArtifact,
287
+ knownFailureIssueValidation
288
+ );
233
289
  attachKnownFailureIssueValidation(
234
290
  enrichedArtifacts.runArtifact,
235
291
  enrichedArtifacts.statusArtifact,