@elench/testkit 0.1.35 → 0.1.37

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.
@@ -28,14 +28,31 @@ describe("runner formatting", () => {
28
28
  formatServiceSummary({
29
29
  completedSuiteCount: 2,
30
30
  failedSuiteCount: 1,
31
+ skippedSuiteCount: 0,
31
32
  suiteCount: 3,
32
33
  totalFileCount: 5,
33
34
  passedFileCount: 3,
35
+ skippedFileCount: 0,
34
36
  notRunFileCount: 1,
35
37
  })
36
38
  ).toBe("1/3 suites passed, 3/5 files passed, 1 suite not run");
37
39
  });
38
40
 
41
+ it("formats skipped suites distinctly from passed suites", () => {
42
+ expect(
43
+ formatServiceSummary({
44
+ completedSuiteCount: 2,
45
+ failedSuiteCount: 0,
46
+ skippedSuiteCount: 1,
47
+ suiteCount: 2,
48
+ totalFileCount: 3,
49
+ passedFileCount: 1,
50
+ skippedFileCount: 2,
51
+ notRunFileCount: 0,
52
+ })
53
+ ).toBe("1/2 suites passed, 1/3 files passed, 1 suite skipped");
54
+ });
55
+
39
56
  it("formats batch descriptors", () => {
40
57
  expect(formatBatchDescriptor({ framework: "k6", tasks: [{}, {}] })).toBe(" (2 files)");
41
58
  expect(formatBatchDescriptor({ framework: "playwright", tasks: [{}] })).toBe(
@@ -71,9 +88,12 @@ describe("runner formatting", () => {
71
88
  failed: true,
72
89
  suiteCount: 2,
73
90
  completedSuiteCount: 2,
91
+ skippedSuiteCount: 0,
74
92
  failedSuiteCount: 1,
75
93
  totalFileCount: 3,
76
94
  passedFileCount: 2,
95
+ skippedFileCount: 0,
96
+ notRunFileCount: 0,
77
97
  durationMs: 20_000,
78
98
  suites: [
79
99
  {
@@ -97,4 +117,33 @@ describe("runner formatting", () => {
97
117
  expect(lines.join("\n")).toContain("worker error: worker broke");
98
118
  expect(lines.at(-1)).toBe("Result: FAILED (1/1 services failed)");
99
119
  });
120
+
121
+ it("marks services with only skipped suites as SKIP", () => {
122
+ const lines = buildRunSummaryLines(
123
+ [
124
+ {
125
+ name: "api",
126
+ skipped: false,
127
+ failed: false,
128
+ suiteCount: 1,
129
+ completedSuiteCount: 1,
130
+ skippedSuiteCount: 1,
131
+ failedSuiteCount: 0,
132
+ totalFileCount: 1,
133
+ passedFileCount: 0,
134
+ skippedFileCount: 1,
135
+ notRunFileCount: 0,
136
+ durationMs: 0,
137
+ suites: [],
138
+ errors: [],
139
+ },
140
+ ],
141
+ 0
142
+ );
143
+
144
+ expect(lines.join("\n")).toContain("suites 1 skipped");
145
+ expect(lines.join("\n")).toContain("files 1 skipped");
146
+ expect(lines.join("\n")).toContain("SKIP api");
147
+ expect(lines.at(-1)).toBe("Result: PASSED");
148
+ });
100
149
  });
@@ -16,7 +16,13 @@ import {
16
16
  } from "./results.mjs";
17
17
  import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
18
18
  import { buildRunSummaryLines, formatError } from "./formatting.mjs";
19
- import { loadTimings, saveTimings, writeRunArtifact, writeStatusArtifact } from "./artifacts.mjs";
19
+ import {
20
+ loadTimings,
21
+ resetResultArtifacts,
22
+ saveTimings,
23
+ writeRunArtifact,
24
+ writeStatusArtifact,
25
+ } from "./artifacts.mjs";
20
26
  import {
21
27
  cleanupRunById,
22
28
  cleanupRuns,
@@ -39,6 +45,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
39
45
  const telemetry = configs[0]?.telemetry || null;
40
46
  const productDir = configs[0]?.productDir || process.cwd();
41
47
  await cleanupStaleRuns(productDir);
48
+ resetResultArtifacts(productDir);
42
49
  const metadata = {
43
50
  git: collectGitMetadata(productDir),
44
51
  host: {
@@ -189,7 +196,7 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
189
196
  return configs.map((config) => {
190
197
  console.log(`\n══ ${config.name} ══`);
191
198
  const suites = applyShard(
192
- collectSuites(config, typeValues, suiteSelectors, opts.fileNames || []),
199
+ collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
193
200
  opts.shard
194
201
  );
195
202
 
@@ -39,7 +39,7 @@ export function resolveRuntimeConfigs(targetConfig, configMap) {
39
39
  return ordered;
40
40
  }
41
41
 
42
- export function collectSuites(config, typeValues, suiteSelectors, fileNames = []) {
42
+ export function collectSuites(config, typeValues, suiteSelectors, fileNames = [], opts = {}) {
43
43
  const selectedFiles = new Set(fileNames.map(normalizePathSeparators));
44
44
  const suites = [];
45
45
  let orderIndex = 0;
@@ -48,17 +48,26 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
48
48
  for (const suite of config.suites[type] || []) {
49
49
  const framework = suite.framework || "k6";
50
50
  const displayType = suiteSelectionType(type, framework);
51
- const files =
51
+ const selectedSuiteFiles =
52
52
  selectedFiles.size === 0
53
53
  ? suite.files
54
54
  : suite.files.filter((file) => selectedFiles.has(normalizePathSeparators(file)));
55
55
  if (!matchesSelectedTypes(displayType, typeValues)) continue;
56
56
  if (!matchesSuiteSelectors(displayType, suite.name, suiteSelectors)) continue;
57
- if (files.length === 0) continue;
57
+ if (selectedSuiteFiles.length === 0) continue;
58
+
59
+ const { files, skippedFiles } = applySkipRules(
60
+ config,
61
+ displayType,
62
+ suite.name,
63
+ selectedSuiteFiles,
64
+ opts
65
+ );
58
66
 
59
67
  suites.push({
60
68
  ...suite,
61
69
  files,
70
+ skippedFiles,
62
71
  framework,
63
72
  type,
64
73
  displayType,
@@ -67,12 +76,13 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
67
76
  weight:
68
77
  suite.testkit?.weight ||
69
78
  (framework === "playwright"
70
- ? Math.max(2, files.length)
79
+ ? Math.max(2, Math.max(1, files.length))
71
80
  : Math.max(1, files.length)),
72
81
  maxFileConcurrency:
73
82
  framework === "k6" || framework === "playwright"
74
83
  ? suite.testkit?.maxFileConcurrency || 1
75
84
  : 1,
85
+ totalFileCount: selectedSuiteFiles.length,
76
86
  });
77
87
  orderIndex += 1;
78
88
  }
@@ -262,6 +272,47 @@ function normalizePathSeparators(filePath) {
262
272
  return String(filePath).split("\\").join("/");
263
273
  }
264
274
 
275
+ function applySkipRules(config, displayType, suiteName, files, opts = {}) {
276
+ if (opts.ignoreSkipRules) {
277
+ return {
278
+ files,
279
+ skippedFiles: [],
280
+ };
281
+ }
282
+ const skip = config.testkit?.skip;
283
+ if (!skip) {
284
+ return {
285
+ files,
286
+ skippedFiles: [],
287
+ };
288
+ }
289
+
290
+ const matchingSuiteRules = skip.suites.filter((rule) =>
291
+ matchesSuiteSelectors(displayType, suiteName, [rule.selector])
292
+ );
293
+ const suiteReason = matchingSuiteRules[0]?.reason || null;
294
+ const runnableFiles = [];
295
+ const skippedFiles = [];
296
+
297
+ for (const file of files) {
298
+ const normalizedFile = normalizePathSeparators(file);
299
+ const reason = skip.fileReasonByPath.get(normalizedFile) || suiteReason;
300
+ if (reason) {
301
+ skippedFiles.push({
302
+ path: normalizedFile,
303
+ reason,
304
+ });
305
+ continue;
306
+ }
307
+ runnableFiles.push(file);
308
+ }
309
+
310
+ return {
311
+ files: runnableFiles,
312
+ skippedFiles,
313
+ };
314
+ }
315
+
265
316
  export function buildGraphDirName(runtimeNames) {
266
317
  const slug = runtimeNames.map(slugSegment).join("__");
267
318
  return slug.length > 0 ? slug : "graph";
@@ -85,6 +85,57 @@ describe("runner-planning", () => {
85
85
  });
86
86
  });
87
87
 
88
+ it("keeps skipped files visible while removing them from runnable work", () => {
89
+ const config = makeConfig("api", {
90
+ suites: {
91
+ integration: [
92
+ {
93
+ name: "billing",
94
+ files: [
95
+ "__testkit__/billing/a.int.testkit.ts",
96
+ "__testkit__/billing/b.int.testkit.ts",
97
+ ],
98
+ },
99
+ ],
100
+ },
101
+ testkit: {
102
+ dependsOn: [],
103
+ skip: {
104
+ fileReasonByPath: new Map([
105
+ ["__testkit__/billing/a.int.testkit.ts", "Billing is stubbed"],
106
+ ]),
107
+ suites: [],
108
+ },
109
+ },
110
+ });
111
+
112
+ expect(collectSuites(config, ["int"], [], [])).toEqual([
113
+ expect.objectContaining({
114
+ name: "billing",
115
+ files: ["__testkit__/billing/b.int.testkit.ts"],
116
+ skippedFiles: [
117
+ {
118
+ path: "__testkit__/billing/a.int.testkit.ts",
119
+ reason: "Billing is stubbed",
120
+ },
121
+ ],
122
+ totalFileCount: 2,
123
+ }),
124
+ ]);
125
+
126
+ expect(collectSuites(config, ["int"], [], [], { ignoreSkipRules: true })).toEqual([
127
+ expect.objectContaining({
128
+ name: "billing",
129
+ files: [
130
+ "__testkit__/billing/a.int.testkit.ts",
131
+ "__testkit__/billing/b.int.testkit.ts",
132
+ ],
133
+ skippedFiles: [],
134
+ totalFileCount: 2,
135
+ }),
136
+ ]);
137
+ });
138
+
88
139
  it("applies shards, builds graphs, queues tasks, and claims batches", () => {
89
140
  const api = makeConfig("api");
90
141
  const frontend = makeConfig("frontend");
@@ -62,7 +62,8 @@ export async function runPlaywrightBatch(targetConfig, batch, lifecycle) {
62
62
  if (fileResult) {
63
63
  return {
64
64
  task,
65
- failed: fileResult.failed,
65
+ failed: fileResult.status === "failed",
66
+ status: fileResult.status,
66
67
  error: fileResult.error,
67
68
  durationMs:
68
69
  fileResult.durationMs > 0
@@ -11,17 +11,22 @@ export function buildStatusArtifact({
11
11
  metadata,
12
12
  }) {
13
13
  const executedResults = results.filter((result) => !result.skipped);
14
+ const effectivelySkippedResults = executedResults.filter(isEffectivelySkippedService);
14
15
  const tests = [];
15
16
 
16
17
  for (const result of executedResults) {
17
18
  for (const suite of result.suites) {
18
19
  for (const file of suite.files) {
19
- tests.push({
20
+ const test = {
20
21
  service: result.name,
21
22
  type: suite.type,
22
23
  path: file.path,
23
24
  status: file.status,
24
- });
25
+ };
26
+ if (file.reason) {
27
+ test.reason = file.reason;
28
+ }
29
+ tests.push(test);
25
30
  }
26
31
  }
27
32
  }
@@ -36,13 +41,17 @@ export function buildStatusArtifact({
36
41
  const summary = {
37
42
  services: {
38
43
  total: executedResults.length,
39
- passed: executedResults.filter((result) => !result.failed).length,
44
+ passed: executedResults.filter(
45
+ (result) => !result.failed && !isEffectivelySkippedService(result)
46
+ ).length,
40
47
  failed: executedResults.filter((result) => result.failed).length,
48
+ skipped: effectivelySkippedResults.length,
41
49
  },
42
50
  tests: {
43
51
  total: tests.length,
44
52
  passed: tests.filter((test) => test.status === "passed").length,
45
53
  failed: tests.filter((test) => test.status === "failed").length,
54
+ skipped: tests.filter((test) => test.status === "skipped").length,
46
55
  notRun: tests.filter((test) => test.status === "not_run").length,
47
56
  },
48
57
  };
@@ -63,7 +72,7 @@ export function buildStatusArtifact({
63
72
  scope.serviceFilter === null;
64
73
 
65
74
  return {
66
- schemaVersion: 2,
75
+ schemaVersion: 3,
67
76
  source: "testkit",
68
77
  notice: "Generated file. Do not edit manually.",
69
78
  product: {
@@ -96,18 +105,21 @@ export function buildRunArtifact({
96
105
  summarizeDbBackend,
97
106
  }) {
98
107
  const executed = results.filter((result) => !result.skipped);
108
+ const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
99
109
  const failedServices = executed.filter((result) => result.failed);
100
110
  const totalSuites = executed.reduce((sum, result) => sum + result.suiteCount, 0);
101
111
  const completedSuites = executed.reduce((sum, result) => sum + result.completedSuiteCount, 0);
112
+ const skippedSuites = executed.reduce((sum, result) => sum + (result.skippedSuiteCount || 0), 0);
102
113
  const failedSuites = executed.reduce((sum, result) => sum + result.failedSuiteCount, 0);
103
114
  const totalFiles = executed.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
104
115
  const passedFiles = executed.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
105
116
  const failedFiles = executed.reduce((sum, result) => sum + (result.failedFileCount || 0), 0);
117
+ const skippedFiles = executed.reduce((sum, result) => sum + (result.skippedFileCount || 0), 0);
106
118
  const notRunFiles = executed.reduce((sum, result) => sum + (result.notRunFileCount || 0), 0);
107
119
  const dbBackend = summarizeDbBackend(results);
108
120
 
109
121
  return {
110
- schemaVersion: 2,
122
+ schemaVersion: 3,
111
123
  source: "testkit",
112
124
  generatedAt: new Date(finishedAt).toISOString(),
113
125
  product: {
@@ -134,19 +146,22 @@ export function buildRunArtifact({
134
146
  summary: {
135
147
  services: {
136
148
  total: executed.length,
137
- passed: executed.length - failedServices.length,
149
+ passed: executed.length - failedServices.length - effectivelySkippedServices.length,
138
150
  failed: failedServices.length,
151
+ skipped: effectivelySkippedServices.length,
139
152
  },
140
153
  suites: {
141
154
  total: totalSuites,
142
155
  completed: completedSuites,
143
- passed: completedSuites - failedSuites,
156
+ passed: completedSuites - failedSuites - skippedSuites,
144
157
  failed: failedSuites,
158
+ skipped: skippedSuites,
145
159
  },
146
160
  files: {
147
161
  total: totalFiles,
148
162
  passed: passedFiles,
149
163
  failed: failedFiles,
164
+ skipped: skippedFiles,
150
165
  notRun: notRunFiles,
151
166
  },
152
167
  },
@@ -156,11 +171,13 @@ export function buildRunArtifact({
156
171
  skipped: result.skipped,
157
172
  suiteCount: result.suiteCount,
158
173
  completedSuiteCount: result.completedSuiteCount,
174
+ skippedSuiteCount: result.skippedSuiteCount,
159
175
  failedSuiteCount: result.failedSuiteCount,
160
176
  totalFileCount: result.totalFileCount,
161
177
  completedFileCount: result.completedFileCount,
162
178
  passedFileCount: result.passedFileCount,
163
179
  failedFileCount: result.failedFileCount,
180
+ skippedFileCount: result.skippedFileCount,
164
181
  notRunFileCount: result.notRunFileCount,
165
182
  durationMs: result.durationMs,
166
183
  totalTaskDurationMs: result.totalTaskDurationMs,
@@ -170,3 +187,13 @@ export function buildRunArtifact({
170
187
  })),
171
188
  };
172
189
  }
190
+
191
+ function isEffectivelySkippedService(result) {
192
+ return (
193
+ !result.skipped &&
194
+ !result.failed &&
195
+ (result.skippedSuiteCount || 0) > 0 &&
196
+ (result.skippedSuiteCount || 0) === result.suiteCount &&
197
+ (result.notRunFileCount || 0) === 0
198
+ );
199
+ }
@@ -10,11 +10,13 @@ describe("runner reporting", () => {
10
10
  skipped: false,
11
11
  suiteCount: 1,
12
12
  completedSuiteCount: 1,
13
+ skippedSuiteCount: 0,
13
14
  failedSuiteCount: 0,
14
15
  totalFileCount: 3,
15
16
  completedFileCount: 3,
16
17
  passedFileCount: 3,
17
18
  failedFileCount: 0,
19
+ skippedFileCount: 0,
18
20
  notRunFileCount: 0,
19
21
  durationMs: 1200,
20
22
  totalTaskDurationMs: 2400,
@@ -28,11 +30,13 @@ describe("runner reporting", () => {
28
30
  skipped: true,
29
31
  suiteCount: 0,
30
32
  completedSuiteCount: 0,
33
+ skippedSuiteCount: 0,
31
34
  failedSuiteCount: 0,
32
35
  totalFileCount: 0,
33
36
  completedFileCount: 0,
34
37
  passedFileCount: 0,
35
38
  failedFileCount: 0,
39
+ skippedFileCount: 0,
36
40
  notRunFileCount: 0,
37
41
  durationMs: 0,
38
42
  totalTaskDurationMs: 0,
@@ -70,11 +74,25 @@ describe("runner reporting", () => {
70
74
  });
71
75
 
72
76
  expect(artifact.product.name).toBe("my-product");
73
- expect(artifact.summary.services.total).toBe(1);
77
+ expect(artifact.schemaVersion).toBe(3);
78
+ expect(artifact.summary.services).toEqual({
79
+ total: 1,
80
+ passed: 1,
81
+ failed: 0,
82
+ skipped: 0,
83
+ });
84
+ expect(artifact.summary.suites).toEqual({
85
+ total: 1,
86
+ completed: 1,
87
+ passed: 1,
88
+ failed: 0,
89
+ skipped: 0,
90
+ });
74
91
  expect(artifact.summary.files).toEqual({
75
92
  total: 3,
76
93
  passed: 3,
77
94
  failed: 0,
95
+ skipped: 0,
78
96
  notRun: 0,
79
97
  });
80
98
  expect(artifact.services[0].durationMs).toBe(1200);
@@ -96,7 +114,11 @@ describe("runner reporting", () => {
96
114
  framework: "k6",
97
115
  files: [
98
116
  { path: "tests/api/integration/a.int.testkit.ts", status: "passed" },
99
- { path: "tests/api/integration/b.int.testkit.ts", status: "failed" },
117
+ {
118
+ path: "tests/api/integration/b.int.testkit.ts",
119
+ status: "skipped",
120
+ reason: "Billing is stubbed",
121
+ },
100
122
  ],
101
123
  },
102
124
  ],
@@ -117,7 +139,7 @@ describe("runner reporting", () => {
117
139
  });
118
140
 
119
141
  expect(status).toEqual({
120
- schemaVersion: 2,
142
+ schemaVersion: 3,
121
143
  source: "testkit",
122
144
  notice: "Generated file. Do not edit manually.",
123
145
  product: {
@@ -141,11 +163,13 @@ describe("runner reporting", () => {
141
163
  total: 1,
142
164
  passed: 0,
143
165
  failed: 1,
166
+ skipped: 0,
144
167
  },
145
168
  tests: {
146
169
  total: 2,
147
170
  passed: 1,
148
- failed: 1,
171
+ failed: 0,
172
+ skipped: 1,
149
173
  notRun: 0,
150
174
  },
151
175
  },
@@ -160,7 +184,8 @@ describe("runner reporting", () => {
160
184
  service: "api",
161
185
  type: "int",
162
186
  path: "tests/api/integration/b.int.testkit.ts",
163
- status: "failed",
187
+ status: "skipped",
188
+ reason: "Billing is stubbed",
164
189
  },
165
190
  ],
166
191
  });
@@ -29,12 +29,12 @@ export function buildServiceTrackers(servicePlans, startedAt) {
29
29
  displayType: suite.displayType || suite.type,
30
30
  framework: suite.framework,
31
31
  orderIndex: suite.orderIndex,
32
- fileCount: suite.files.length,
32
+ fileCount: suite.totalFileCount ?? suite.files.length,
33
33
  completedFileCount: 0,
34
34
  failedFiles: [],
35
35
  failedFileSet: new Set(),
36
- fileResultsByPath: new Map(
37
- suite.files.map((file) => {
36
+ fileResultsByPath: new Map([
37
+ ...suite.files.map((file) => {
38
38
  const normalizedPath = normalizePathSeparators(file);
39
39
  return [
40
40
  normalizedPath,
@@ -43,11 +43,25 @@ export function buildServiceTrackers(servicePlans, startedAt) {
43
43
  failed: false,
44
44
  durationMs: 0,
45
45
  error: null,
46
+ reason: null,
46
47
  status: "not_run",
48
+ artifacts: [],
47
49
  },
48
50
  ];
49
- })
50
- ),
51
+ }),
52
+ ...(suite.skippedFiles || []).map((file) => [
53
+ file.path,
54
+ {
55
+ path: file.path,
56
+ failed: false,
57
+ durationMs: 0,
58
+ error: null,
59
+ reason: file.reason,
60
+ status: "skipped",
61
+ artifacts: [],
62
+ },
63
+ ]),
64
+ ]),
51
65
  durationMs: 0,
52
66
  error: null,
53
67
  }));
@@ -93,25 +107,32 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
93
107
  : Math.max(tracker.lastTaskAt, outcomeFinishedAt);
94
108
  tracker.totalTaskDurationMs += outcomeDurationMs;
95
109
 
96
- suite.completedFileCount += 1;
97
110
  suite.durationMs += outcomeDurationMs;
98
111
  const normalizedPath = normalizePathSeparators(task.file);
99
112
  const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
113
+ const status = normalizeOutcomeStatus(outcome);
114
+ if (status !== "skipped") {
115
+ suite.completedFileCount += 1;
116
+ }
100
117
  if (existingFileResult) {
101
- existingFileResult.failed = outcome.failed;
118
+ existingFileResult.failed = status === "failed";
102
119
  existingFileResult.durationMs = outcomeDurationMs;
103
120
  existingFileResult.error = outcome.error;
104
- existingFileResult.status = outcome.failed ? "failed" : "passed";
121
+ existingFileResult.reason = outcome.reason || null;
122
+ existingFileResult.status = status;
123
+ existingFileResult.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : [];
105
124
  } else {
106
125
  suite.fileResultsByPath.set(normalizedPath, {
107
126
  path: normalizedPath,
108
- failed: outcome.failed,
127
+ failed: status === "failed",
109
128
  durationMs: outcomeDurationMs,
110
129
  error: outcome.error,
111
- status: outcome.failed ? "failed" : "passed",
130
+ reason: outcome.reason || null,
131
+ status,
132
+ artifacts: Array.isArray(outcome.artifacts) ? outcome.artifacts : [],
112
133
  });
113
134
  }
114
- if (outcome.failed && !suite.failedFileSet.has(task.file)) {
135
+ if (status === "failed" && !suite.failedFileSet.has(task.file)) {
115
136
  suite.failedFileSet.add(task.file);
116
137
  suite.failedFiles.push(task.file);
117
138
  }
@@ -145,7 +166,14 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
145
166
  skipped: true,
146
167
  suiteCount: 0,
147
168
  completedSuiteCount: 0,
169
+ skippedSuiteCount: 0,
148
170
  failedSuiteCount: 0,
171
+ totalFileCount: 0,
172
+ completedFileCount: 0,
173
+ passedFileCount: 0,
174
+ failedFileCount: 0,
175
+ skippedFileCount: 0,
176
+ notRunFileCount: 0,
149
177
  durationMs: 0,
150
178
  totalTaskDurationMs: 0,
151
179
  suites: [],
@@ -158,14 +186,18 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
158
186
  .map((suite) => finalizeSuite(suite));
159
187
 
160
188
  const completedSuiteCount = suites.filter(
161
- (suite) => suite.completedFileCount === suite.fileCount
189
+ (suite) => suite.completedFileCount + suite.skippedFileCount === suite.fileCount
190
+ ).length;
191
+ const skippedSuiteCount = suites.filter(
192
+ (suite) => suite.skippedFileCount === suite.fileCount && suite.fileCount > 0
162
193
  ).length;
163
194
  const failedSuiteCount = suites.filter((suite) => suite.failedFileCount > 0).length;
164
195
  const totalFileCount = suites.reduce((sum, suite) => sum + suite.fileCount, 0);
165
196
  const completedFileCount = suites.reduce((sum, suite) => sum + suite.completedFileCount, 0);
166
197
  const failedFileCount = suites.reduce((sum, suite) => sum + suite.failedFileCount, 0);
167
198
  const passedFileCount = suites.reduce((sum, suite) => sum + suite.passedFileCount, 0);
168
- const notRunFileCount = totalFileCount - completedFileCount;
199
+ const skippedFileCount = suites.reduce((sum, suite) => sum + suite.skippedFileCount, 0);
200
+ const notRunFileCount = totalFileCount - completedFileCount - skippedFileCount;
169
201
  const totalTaskDurationMs =
170
202
  tracker.totalTaskDurationMs || suites.reduce((sum, suite) => sum + suite.durationMs, 0);
171
203
  const durationMs =
@@ -183,11 +215,13 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
183
215
  skipped: false,
184
216
  suiteCount: tracker.suiteCount,
185
217
  completedSuiteCount,
218
+ skippedSuiteCount,
186
219
  failedSuiteCount,
187
220
  totalFileCount,
188
221
  completedFileCount,
189
222
  passedFileCount,
190
223
  failedFileCount,
224
+ skippedFileCount,
191
225
  notRunFileCount,
192
226
  durationMs,
193
227
  totalTaskDurationMs,
@@ -212,6 +246,10 @@ function finalizeSuite(suite) {
212
246
  status: file.status,
213
247
  durationMs: file.durationMs,
214
248
  error: file.error,
249
+ reason: file.reason,
250
+ ...(Array.isArray(file.artifacts) && file.artifacts.length > 0
251
+ ? { artifacts: file.artifacts }
252
+ : {}),
215
253
  }));
216
254
 
217
255
  return {
@@ -223,7 +261,11 @@ function finalizeSuite(suite) {
223
261
  completedFileCount: suite.completedFileCount,
224
262
  passedFileCount: files.filter((file) => file.status === "passed").length,
225
263
  failedFileCount: suite.failedFiles.length,
226
- notRunFileCount: suite.fileCount - suite.completedFileCount,
264
+ skippedFileCount: files.filter((file) => file.status === "skipped").length,
265
+ notRunFileCount:
266
+ suite.fileCount -
267
+ suite.completedFileCount -
268
+ files.filter((file) => file.status === "skipped").length,
227
269
  failedFiles: suite.failedFiles,
228
270
  durationMs: suite.durationMs,
229
271
  error: suite.error,
@@ -239,3 +281,8 @@ function formatFrameworkForArtifact(framework) {
239
281
  if (framework === "k6") return "default";
240
282
  return framework;
241
283
  }
284
+
285
+ function normalizeOutcomeStatus(outcome) {
286
+ if (outcome?.status === "skipped") return "skipped";
287
+ return outcome?.failed ? "failed" : "passed";
288
+ }