@elench/testkit 0.1.34 → 0.1.36

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.
@@ -7,12 +7,18 @@ export function formatDuration(durationMs) {
7
7
  }
8
8
 
9
9
  export function formatServiceSummary(result) {
10
- const passedSuites = result.completedSuiteCount - result.failedSuiteCount;
10
+ const skippedSuites = result.skippedSuiteCount || 0;
11
+ const passedSuites = result.completedSuiteCount - result.failedSuiteCount - skippedSuites;
11
12
  const notRunSuites = result.suiteCount - result.completedSuiteCount;
12
13
  let detail = `${passedSuites}/${result.suiteCount} suites passed`;
13
14
  if ((result.totalFileCount || 0) > 0) {
14
15
  detail += `, ${result.passedFileCount}/${result.totalFileCount} files passed`;
15
16
  }
17
+ if (skippedSuites > 0) {
18
+ detail += `, ${skippedSuites} ${pluralize(skippedSuites, "suite", "suites")} skipped`;
19
+ } else if ((result.skippedFileCount || 0) > 0) {
20
+ detail += `, ${result.skippedFileCount} ${pluralize(result.skippedFileCount, "file", "files")} skipped`;
21
+ }
16
22
  if (notRunSuites > 0) {
17
23
  detail += `, ${notRunSuites} ${pluralize(notRunSuites, "suite", "suites")} not run`;
18
24
  } else if ((result.notRunFileCount || 0) > 0) {
@@ -66,10 +72,18 @@ export function buildRunSummaryLines(results, durationMs) {
66
72
  (sum, result) => sum + result.completedSuiteCount,
67
73
  0
68
74
  );
75
+ const skippedSuites = executedServices.reduce(
76
+ (sum, result) => sum + (result.skippedSuiteCount || 0),
77
+ 0
78
+ );
69
79
  const failedSuites = executedServices.reduce((sum, result) => sum + result.failedSuiteCount, 0);
70
- const passedSuites = completedSuites - failedSuites;
80
+ const passedSuites = completedSuites - failedSuites - skippedSuites;
71
81
  const totalFiles = executedServices.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
72
82
  const passedFiles = executedServices.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
83
+ const skippedFiles = executedServices.reduce(
84
+ (sum, result) => sum + (result.skippedFileCount || 0),
85
+ 0
86
+ );
73
87
  const lines = [
74
88
  "",
75
89
  "══ Summary ══",
@@ -77,6 +91,8 @@ export function buildRunSummaryLines(results, durationMs) {
77
91
  `services ${passedServices.length}/${executedServices.length} passed`,
78
92
  `suites ${passedSuites}/${totalSuites} passed`,
79
93
  totalFiles > 0 ? `files ${passedFiles}/${totalFiles} passed` : null,
94
+ skippedSuites > 0 ? `suites ${skippedSuites} skipped` : null,
95
+ skippedFiles > 0 ? `files ${skippedFiles} skipped` : null,
80
96
  skippedServices.length > 0 ? `${skippedServices.length} skipped` : null,
81
97
  `duration ${formatDuration(durationMs)}`,
82
98
  ]
@@ -85,8 +101,14 @@ export function buildRunSummaryLines(results, durationMs) {
85
101
  ];
86
102
 
87
103
  for (const result of results) {
88
- const status = result.skipped ? "SKIP" : result.failed ? "FAIL" : "PASS";
89
- const detail = result.skipped ? "no matching suites" : formatServiceSummary(result);
104
+ const status = isServiceEffectivelySkipped(result)
105
+ ? "SKIP"
106
+ : result.failed
107
+ ? "FAIL"
108
+ : "PASS";
109
+ const detail = result.skipped
110
+ ? "no matching suites"
111
+ : formatServiceSummary(result);
90
112
  lines.push(
91
113
  `${status.padEnd(4)} ${result.name.padEnd(longestServiceName(results))} ${detail} · ${formatDuration(result.durationMs)}`
92
114
  );
@@ -127,3 +149,13 @@ function sanitizeErrorMessage(message) {
127
149
  function pluralize(value, singular, plural) {
128
150
  return value === 1 ? singular : plural;
129
151
  }
152
+
153
+ function isServiceEffectivelySkipped(result) {
154
+ if (result.skipped) return true;
155
+ return (
156
+ !result.failed &&
157
+ (result.skippedSuiteCount || 0) > 0 &&
158
+ (result.skippedSuiteCount || 0) === result.suiteCount &&
159
+ (result.notRunFileCount || 0) === 0
160
+ );
161
+ }
@@ -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
  });
@@ -33,7 +33,7 @@ import { createWorker, runWorker } from "./worker-loop.mjs";
33
33
  import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
34
34
  import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
35
35
 
36
- export async function runAll(configs, suiteType, suiteNames, opts, allConfigs = configs) {
36
+ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfigs = configs) {
37
37
  const configMap = new Map(allConfigs.map((config) => [config.name, config]));
38
38
  const startedAt = Date.now();
39
39
  const telemetry = configs[0]?.telemetry || null;
@@ -51,9 +51,8 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
51
51
  if (requestedFiles.length > 0) {
52
52
  const unmatchedFiles = findUnmatchedRequestedFiles(
53
53
  configs,
54
- suiteType,
55
- suiteNames,
56
- opts.framework || "all",
54
+ typeValues,
55
+ suiteSelectors,
57
56
  requestedFiles,
58
57
  collectSuites,
59
58
  normalizePathSeparators
@@ -69,9 +68,9 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
69
68
  opts.writeStatus &&
70
69
  !opts.allowPartialStatus &&
71
70
  !isFullRunSelection(
72
- suiteNames,
71
+ typeValues,
72
+ suiteSelectors,
73
73
  requestedFiles,
74
- opts.framework || "all",
75
74
  opts.shard || null,
76
75
  opts.serviceFilter || null
77
76
  )
@@ -82,7 +81,7 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
82
81
  );
83
82
  }
84
83
 
85
- const servicePlans = collectServicePlans(configs, configMap, suiteType, suiteNames, opts);
84
+ const servicePlans = collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts);
86
85
  const trackers = buildServiceTrackers(servicePlans, startedAt);
87
86
  const executedPlans = servicePlans.filter((plan) => !plan.skipped);
88
87
  let workerCount = 0;
@@ -144,10 +143,9 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
144
143
  finishedAt,
145
144
  requestedJobs: opts.jobs || 1,
146
145
  workerCount,
147
- suiteType,
148
- suiteNames,
146
+ typeValues,
147
+ suiteSelectors,
149
148
  fileNames: requestedFiles,
150
- framework: opts.framework || "all",
151
149
  shard: opts.shard || null,
152
150
  serviceFilter: opts.serviceFilter || null,
153
151
  metadata,
@@ -161,10 +159,9 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
161
159
  buildStatusArtifact({
162
160
  productDir,
163
161
  results,
164
- suiteType,
165
- suiteNames,
162
+ typeValues,
163
+ suiteSelectors,
166
164
  fileNames: requestedFiles,
167
- framework: opts.framework || "all",
168
165
  shard: opts.shard || null,
169
166
  serviceFilter: opts.serviceFilter || null,
170
167
  metadata,
@@ -188,17 +185,17 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
188
185
  }
189
186
  }
190
187
 
191
- function collectServicePlans(configs, configMap, suiteType, suiteNames, opts) {
188
+ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts) {
192
189
  return configs.map((config) => {
193
190
  console.log(`\n══ ${config.name} ══`);
194
191
  const suites = applyShard(
195
- collectSuites(config, suiteType, suiteNames, opts.framework, opts.fileNames || []),
192
+ collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
196
193
  opts.shard
197
194
  );
198
195
 
199
196
  if (suites.length === 0) {
200
197
  console.log(
201
- `No test files for ${config.name} type="${suiteType}" suites=${suiteNames.join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"} — skipping`
198
+ `No test files for ${config.name} types=${typeValues.join(",") || "all"} suites=${suiteSelectors.map((selector) => selector.raw).join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"} — skipping`
202
199
  );
203
200
  return {
204
201
  config,
@@ -1,4 +1,9 @@
1
1
  import { buildTimingKey, estimateTaskDuration } from "../timing/index.mjs";
2
+ import {
3
+ matchesSelectedTypes,
4
+ matchesSuiteSelectors,
5
+ suiteSelectionType,
6
+ } from "./suite-selection.mjs";
2
7
 
3
8
  const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
4
9
 
@@ -34,44 +39,50 @@ export function resolveRuntimeConfigs(targetConfig, configMap) {
34
39
  return ordered;
35
40
  }
36
41
 
37
- export function collectSuites(config, suiteType, suiteNames, frameworkFilter, fileNames = []) {
38
- const types =
39
- suiteType === "all"
40
- ? orderedTypes(Object.keys(config.suites))
41
- : [suiteType === "int" ? "integration" : suiteType];
42
-
43
- const selectedNames = new Set(suiteNames);
42
+ export function collectSuites(config, typeValues, suiteSelectors, fileNames = [], opts = {}) {
44
43
  const selectedFiles = new Set(fileNames.map(normalizePathSeparators));
45
44
  const suites = [];
46
45
  let orderIndex = 0;
47
46
 
48
- for (const type of types) {
47
+ for (const type of orderedTypes(Object.keys(config.suites))) {
49
48
  for (const suite of config.suites[type] || []) {
50
49
  const framework = suite.framework || "k6";
51
- const files =
50
+ const displayType = suiteSelectionType(type, framework);
51
+ const selectedSuiteFiles =
52
52
  selectedFiles.size === 0
53
53
  ? suite.files
54
54
  : suite.files.filter((file) => selectedFiles.has(normalizePathSeparators(file)));
55
- if (selectedNames.size > 0 && !selectedNames.has(suite.name)) continue;
56
- if (frameworkFilter && frameworkFilter !== "all" && framework !== frameworkFilter) continue;
57
- if (files.length === 0) continue;
55
+ if (!matchesSelectedTypes(displayType, typeValues)) continue;
56
+ if (!matchesSuiteSelectors(displayType, suite.name, suiteSelectors)) 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,
73
+ displayType,
64
74
  orderIndex,
65
75
  sortKey: `${type}:${suite.name}`,
66
76
  weight:
67
77
  suite.testkit?.weight ||
68
78
  (framework === "playwright"
69
- ? Math.max(2, files.length)
79
+ ? Math.max(2, Math.max(1, files.length))
70
80
  : Math.max(1, files.length)),
71
81
  maxFileConcurrency:
72
82
  framework === "k6" || framework === "playwright"
73
83
  ? suite.testkit?.maxFileConcurrency || 1
74
84
  : 1,
85
+ totalFileCount: selectedSuiteFiles.length,
75
86
  });
76
87
  orderIndex += 1;
77
88
  }
@@ -261,6 +272,47 @@ function normalizePathSeparators(filePath) {
261
272
  return String(filePath).split("\\").join("/");
262
273
  }
263
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
+
264
316
  export function buildGraphDirName(runtimeNames) {
265
317
  const slug = runtimeNames.map(slugSegment).join("__");
266
318
  return slug.length > 0 ? slug : "graph";
@@ -38,7 +38,7 @@ describe("runner-planning", () => {
38
38
  expect(() => resolveRuntimeConfigs(frontend, configMap)).toThrow("Dependency cycle");
39
39
  });
40
40
 
41
- it("collects suites with aliases, weights, and framework filters", () => {
41
+ it("collects suites with user-facing type selection", () => {
42
42
  const config = makeConfig("api", {
43
43
  suites: {
44
44
  integration: [
@@ -57,15 +57,17 @@ describe("runner-planning", () => {
57
57
  },
58
58
  });
59
59
 
60
- expect(collectSuites(config, "int", [], "all")[0]).toMatchObject({
60
+ expect(collectSuites(config, ["int"], [], [])[0]).toMatchObject({
61
61
  name: "health",
62
62
  type: "integration",
63
+ displayType: "int",
63
64
  framework: "k6",
64
65
  weight: 1,
65
66
  });
66
67
 
67
- expect(collectSuites(config, "all", [], "playwright")[0]).toMatchObject({
68
+ expect(collectSuites(config, ["pw"], [], [])[0]).toMatchObject({
68
69
  name: "auth",
70
+ displayType: "pw",
69
71
  framework: "playwright",
70
72
  weight: 2,
71
73
  });
@@ -73,9 +75,8 @@ describe("runner-planning", () => {
73
75
  expect(
74
76
  collectSuites(
75
77
  config,
76
- "all",
78
+ ["all"],
77
79
  [],
78
- "all",
79
80
  ["tests/frontend/e2e/signup.pw.testkit.ts"]
80
81
  )[0]
81
82
  ).toMatchObject({
@@ -84,6 +85,57 @@ describe("runner-planning", () => {
84
85
  });
85
86
  });
86
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
+
87
139
  it("applies shards, builds graphs, queues tasks, and claims batches", () => {
88
140
  const api = makeConfig("api");
89
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
@@ -3,26 +3,30 @@ import path from "path";
3
3
  export function buildStatusArtifact({
4
4
  productDir,
5
5
  results,
6
- suiteType,
7
- suiteNames,
6
+ typeValues,
7
+ suiteSelectors,
8
8
  fileNames,
9
- framework,
10
9
  shard,
11
10
  serviceFilter,
12
11
  metadata,
13
12
  }) {
14
13
  const executedResults = results.filter((result) => !result.skipped);
14
+ const effectivelySkippedResults = executedResults.filter(isEffectivelySkippedService);
15
15
  const tests = [];
16
16
 
17
17
  for (const result of executedResults) {
18
18
  for (const suite of result.suites) {
19
19
  for (const file of suite.files) {
20
- tests.push({
20
+ const test = {
21
21
  service: result.name,
22
22
  type: suite.type,
23
23
  path: file.path,
24
24
  status: file.status,
25
- });
25
+ };
26
+ if (file.reason) {
27
+ test.reason = file.reason;
28
+ }
29
+ tests.push(test);
26
30
  }
27
31
  }
28
32
  }
@@ -37,34 +41,38 @@ export function buildStatusArtifact({
37
41
  const summary = {
38
42
  services: {
39
43
  total: executedResults.length,
40
- passed: executedResults.filter((result) => !result.failed).length,
44
+ passed: executedResults.filter(
45
+ (result) => !result.failed && !isEffectivelySkippedService(result)
46
+ ).length,
41
47
  failed: executedResults.filter((result) => result.failed).length,
48
+ skipped: effectivelySkippedResults.length,
42
49
  },
43
50
  tests: {
44
51
  total: tests.length,
45
52
  passed: tests.filter((test) => test.status === "passed").length,
46
53
  failed: tests.filter((test) => test.status === "failed").length,
54
+ skipped: tests.filter((test) => test.status === "skipped").length,
47
55
  notRun: tests.filter((test) => test.status === "not_run").length,
48
56
  },
49
57
  };
50
58
 
51
59
  const scope = {
52
- suiteType,
53
- suiteNames: [...(suiteNames || [])].sort(),
60
+ types: [...(typeValues || ["all"])].sort(),
61
+ suiteSelectors: [...(suiteSelectors || [])].map((selector) => selector.raw).sort(),
54
62
  fileNames: [...(fileNames || [])].sort(),
55
- framework: formatFrameworkForArtifact(framework || "all"),
56
63
  shard: shard || null,
57
64
  serviceFilter: serviceFilter || null,
58
65
  };
59
66
  scope.isFullRun =
60
- scope.suiteNames.length === 0 &&
67
+ scope.types.length === 1 &&
68
+ scope.types[0] === "all" &&
69
+ scope.suiteSelectors.length === 0 &&
61
70
  scope.fileNames.length === 0 &&
62
- scope.framework === "all" &&
63
71
  scope.shard === null &&
64
72
  scope.serviceFilter === null;
65
73
 
66
74
  return {
67
- schemaVersion: 1,
75
+ schemaVersion: 3,
68
76
  source: "testkit",
69
77
  notice: "Generated file. Do not edit manually.",
70
78
  product: {
@@ -88,28 +96,30 @@ export function buildRunArtifact({
88
96
  finishedAt,
89
97
  requestedJobs,
90
98
  workerCount,
91
- suiteType,
92
- suiteNames,
99
+ typeValues,
100
+ suiteSelectors,
93
101
  fileNames,
94
- framework,
95
102
  shard,
96
103
  serviceFilter,
97
104
  metadata,
98
105
  summarizeDbBackend,
99
106
  }) {
100
107
  const executed = results.filter((result) => !result.skipped);
108
+ const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
101
109
  const failedServices = executed.filter((result) => result.failed);
102
110
  const totalSuites = executed.reduce((sum, result) => sum + result.suiteCount, 0);
103
111
  const completedSuites = executed.reduce((sum, result) => sum + result.completedSuiteCount, 0);
112
+ const skippedSuites = executed.reduce((sum, result) => sum + (result.skippedSuiteCount || 0), 0);
104
113
  const failedSuites = executed.reduce((sum, result) => sum + result.failedSuiteCount, 0);
105
114
  const totalFiles = executed.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
106
115
  const passedFiles = executed.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
107
116
  const failedFiles = executed.reduce((sum, result) => sum + (result.failedFileCount || 0), 0);
117
+ const skippedFiles = executed.reduce((sum, result) => sum + (result.skippedFileCount || 0), 0);
108
118
  const notRunFiles = executed.reduce((sum, result) => sum + (result.notRunFileCount || 0), 0);
109
119
  const dbBackend = summarizeDbBackend(results);
110
120
 
111
121
  return {
112
- schemaVersion: 1,
122
+ schemaVersion: 3,
113
123
  source: "testkit",
114
124
  generatedAt: new Date(finishedAt).toISOString(),
115
125
  product: {
@@ -126,10 +136,9 @@ export function buildRunArtifact({
126
136
  requestedJobs,
127
137
  workerCount,
128
138
  dbBackend,
129
- suiteType,
130
- suiteNames,
139
+ types: typeValues,
140
+ suiteSelectors: suiteSelectors.map((selector) => selector.raw),
131
141
  fileNames,
132
- framework: formatFrameworkForArtifact(framework),
133
142
  shard,
134
143
  serviceFilter,
135
144
  testkitVersion: metadata.testkitVersion,
@@ -137,19 +146,22 @@ export function buildRunArtifact({
137
146
  summary: {
138
147
  services: {
139
148
  total: executed.length,
140
- passed: executed.length - failedServices.length,
149
+ passed: executed.length - failedServices.length - effectivelySkippedServices.length,
141
150
  failed: failedServices.length,
151
+ skipped: effectivelySkippedServices.length,
142
152
  },
143
153
  suites: {
144
154
  total: totalSuites,
145
155
  completed: completedSuites,
146
- passed: completedSuites - failedSuites,
156
+ passed: completedSuites - failedSuites - skippedSuites,
147
157
  failed: failedSuites,
158
+ skipped: skippedSuites,
148
159
  },
149
160
  files: {
150
161
  total: totalFiles,
151
162
  passed: passedFiles,
152
163
  failed: failedFiles,
164
+ skipped: skippedFiles,
153
165
  notRun: notRunFiles,
154
166
  },
155
167
  },
@@ -159,11 +171,13 @@ export function buildRunArtifact({
159
171
  skipped: result.skipped,
160
172
  suiteCount: result.suiteCount,
161
173
  completedSuiteCount: result.completedSuiteCount,
174
+ skippedSuiteCount: result.skippedSuiteCount,
162
175
  failedSuiteCount: result.failedSuiteCount,
163
176
  totalFileCount: result.totalFileCount,
164
177
  completedFileCount: result.completedFileCount,
165
178
  passedFileCount: result.passedFileCount,
166
179
  failedFileCount: result.failedFileCount,
180
+ skippedFileCount: result.skippedFileCount,
167
181
  notRunFileCount: result.notRunFileCount,
168
182
  durationMs: result.durationMs,
169
183
  totalTaskDurationMs: result.totalTaskDurationMs,
@@ -174,7 +188,12 @@ export function buildRunArtifact({
174
188
  };
175
189
  }
176
190
 
177
- function formatFrameworkForArtifact(framework) {
178
- if (framework === "k6") return "default";
179
- return framework;
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
+ );
180
199
  }