@elench/testkit 0.1.39 → 0.1.41

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/README.md +43 -13
  2. package/lib/cli/args.mjs +5 -3
  3. package/lib/cli/args.test.mjs +5 -5
  4. package/lib/cli/index.mjs +9 -15
  5. package/lib/config/index.mjs +72 -24
  6. package/lib/database/index.mjs +19 -7
  7. package/lib/database/naming.mjs +2 -2
  8. package/lib/database/naming.test.mjs +2 -2
  9. package/lib/runner/default-runtime-runner.mjs +63 -43
  10. package/lib/runner/execution-config.mjs +24 -64
  11. package/lib/runner/execution-config.test.mjs +30 -72
  12. package/lib/runner/formatting.mjs +0 -15
  13. package/lib/runner/formatting.test.mjs +0 -18
  14. package/lib/runner/lifecycle.mjs +7 -7
  15. package/lib/runner/orchestrator.mjs +9 -10
  16. package/lib/runner/planning.mjs +42 -136
  17. package/lib/runner/planning.test.mjs +70 -174
  18. package/lib/runner/playwright-config.mjs +8 -2
  19. package/lib/runner/playwright-config.test.mjs +20 -5
  20. package/lib/runner/playwright-runner.mjs +32 -54
  21. package/lib/runner/readiness.mjs +2 -2
  22. package/lib/runner/reporting.mjs +3 -3
  23. package/lib/runner/reporting.test.mjs +4 -5
  24. package/lib/runner/results.mjs +1 -1
  25. package/lib/runner/results.test.mjs +1 -1
  26. package/lib/runner/runtime-contexts.mjs +20 -24
  27. package/lib/runner/runtime-manager.mjs +181 -0
  28. package/lib/runner/runtime-manager.test.mjs +181 -0
  29. package/lib/runner/services.mjs +4 -4
  30. package/lib/runner/state.mjs +1 -2
  31. package/lib/runner/state.test.mjs +2 -4
  32. package/lib/runner/template.mjs +90 -60
  33. package/lib/runner/template.test.mjs +59 -27
  34. package/lib/runner/worker-loop.mjs +29 -32
  35. package/lib/runtime/index.d.ts +11 -0
  36. package/lib/runtime/index.mjs +34 -0
  37. package/lib/setup/index.d.ts +15 -10
  38. package/lib/shared/file-timeout.mjs +107 -0
  39. package/lib/shared/file-timeout.test.mjs +64 -0
  40. package/package.json +1 -1
  41. package/lib/runner/stack-manager.mjs +0 -146
@@ -3,89 +3,82 @@ import path from "path";
3
3
  import { execa } from "execa";
4
4
  import { bundleK6File } from "../bundler/index.mjs";
5
5
  import { resolveK6Binary } from "../config/index.mjs";
6
+ import {
7
+ buildFileTimeoutEnv,
8
+ formatFileTimeoutBudgetError,
9
+ } from "../shared/file-timeout.mjs";
6
10
  import { persistTaskArtifacts } from "./artifacts.mjs";
7
- import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
8
11
  import { determineDefaultRuntimeFailure } from "./default-runtime-errors.mjs";
9
- import { formatBatchDescriptor } from "./formatting.mjs";
10
- import { buildExecutionEnv } from "./template.mjs";
12
+ import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
11
13
  import { readDatabaseUrl } from "./state-io.mjs";
14
+ import { buildTaskExecutionEnv } from "./template.mjs";
12
15
 
13
- export async function runHttpK6Batch(targetConfig, batch, lifecycle) {
16
+ export async function runHttpK6Task(targetConfig, task, lifecycle, lease) {
14
17
  const baseUrl = targetConfig.testkit.local?.baseUrl;
15
18
  if (!baseUrl) {
16
19
  throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP suites`);
17
20
  }
18
21
 
19
- console.log(
20
- `\n── ${targetConfig.stackLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
21
- );
22
-
23
- return Promise.all(
24
- batch.tasks.map((task) => runHttpK6Task(targetConfig, task, baseUrl, lifecycle))
25
- );
26
- }
27
-
28
- export async function runDalBatch(targetConfig, batch, lifecycle) {
29
- const databaseUrl = readDatabaseUrl(targetConfig.stateDir);
30
- if (!databaseUrl) {
31
- throw new Error(`Service "${targetConfig.name}" requires a database for DAL suites`);
32
- }
33
-
34
- console.log(
35
- `\n── ${targetConfig.stackLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
36
- );
37
-
38
- return Promise.all(
39
- batch.tasks.map((task) => runDalTask(targetConfig, task, databaseUrl, lifecycle))
40
- );
41
- }
42
-
43
- async function runHttpK6Task(targetConfig, task, baseUrl, lifecycle) {
44
22
  const bundledFile = await bundleK6File({
45
23
  productDir: targetConfig.productDir,
46
24
  serviceName: targetConfig.name,
47
25
  sourceFile: path.join(targetConfig.productDir, task.file),
48
26
  });
49
- console.log(`·· ${targetConfig.stackLabel}:${task.suiteName} → ${task.file}`);
27
+ console.log(`·· ${targetConfig.runtimeLabel}:${task.suiteName} → ${task.file}`);
50
28
  return runDefaultRuntimeTask(
51
29
  targetConfig,
52
30
  task,
31
+ lease,
53
32
  ["run", "--address", "127.0.0.1:0", "-e", `BASE_URL=${baseUrl}`, bundledFile],
54
33
  lifecycle
55
34
  );
56
35
  }
57
36
 
58
- async function runDalTask(targetConfig, task, databaseUrl, lifecycle) {
37
+ export async function runDalTask(targetConfig, task, lifecycle, lease) {
38
+ const databaseUrl = readDatabaseUrl(targetConfig.stateDir);
39
+ if (!databaseUrl) {
40
+ throw new Error(`Service "${targetConfig.name}" requires a database for DAL suites`);
41
+ }
42
+
59
43
  const bundledFile = await bundleK6File({
60
44
  productDir: targetConfig.productDir,
61
45
  serviceName: targetConfig.name,
62
46
  sourceFile: path.join(targetConfig.productDir, task.file),
63
47
  });
64
- console.log(`·· ${targetConfig.stackLabel}:${task.suiteName} → ${task.file}`);
48
+ console.log(`·· ${targetConfig.runtimeLabel}:${task.suiteName} → ${task.file}`);
65
49
  return runDefaultRuntimeTask(
66
50
  targetConfig,
67
51
  task,
52
+ lease,
68
53
  ["run", "--address", "127.0.0.1:0", "-e", `DATABASE_URL=${databaseUrl}`, bundledFile],
69
54
  lifecycle
70
55
  );
71
56
  }
72
57
 
73
- export async function runDefaultRuntimeTask(targetConfig, task, args, lifecycle, firstLine) {
58
+ export async function runDefaultRuntimeTask(targetConfig, task, lease, args, lifecycle, firstLine) {
74
59
  const k6Binary = resolveK6Binary();
75
- const summaryFile = buildDefaultRuntimeSummaryPath(targetConfig, task);
60
+ const getFirstLine = firstLine || defaultFirstLine;
61
+ const summaryFile = buildDefaultRuntimeSummaryPath(lease, task);
76
62
  fs.mkdirSync(path.dirname(summaryFile), { recursive: true });
77
63
  const startedAt = Date.now();
78
- const result = await execa(
64
+ const fileTimeoutSeconds = targetConfig.testkit.execution.fileTimeoutSeconds;
65
+ const subprocess = execa(
79
66
  k6Binary,
80
67
  [...args.slice(0, 1), "--summary-export", summaryFile, ...args.slice(1)],
81
68
  {
82
69
  cwd: targetConfig.productDir,
83
- env: buildExecutionEnv(targetConfig, {}, process.env),
70
+ env: buildTaskExecutionEnv(
71
+ targetConfig,
72
+ lease,
73
+ buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
74
+ process.env
75
+ ),
84
76
  reject: false,
85
77
  cancelSignal: lifecycle.signal,
86
78
  forceKillAfterDelay: 5_000,
87
79
  }
88
80
  );
81
+ const { result, timedOut } = await settleSubprocess(subprocess, fileTimeoutSeconds);
89
82
 
90
83
  const stdout = parseDefaultRuntimeOutput(result.stdout || "");
91
84
  const stderr = parseDefaultRuntimeOutput(result.stderr || "");
@@ -98,7 +91,9 @@ export async function runDefaultRuntimeTask(targetConfig, task, args, lifecycle,
98
91
  task,
99
92
  [...stdout.artifacts, ...stderr.artifacts]
100
93
  );
101
- const runtimeError = determineDefaultRuntimeFailure(result, summary, firstLine);
94
+ const runtimeError = timedOut
95
+ ? formatFileTimeoutBudgetError(fileTimeoutSeconds)
96
+ : determineDefaultRuntimeFailure(result, summary, getFirstLine);
102
97
  const finishedAt = Date.now();
103
98
 
104
99
  return {
@@ -112,12 +107,30 @@ export async function runDefaultRuntimeTask(targetConfig, task, args, lifecycle,
112
107
  };
113
108
  }
114
109
 
115
- export function buildDefaultRuntimeSummaryPath(targetConfig, task) {
116
- return path.join(
117
- targetConfig.stateDir || path.join(targetConfig.productDir, ".testkit"),
118
- "_runtime",
119
- `task-${task.id}.summary.json`
120
- );
110
+ export async function settleSubprocess(subprocess, fileTimeoutSeconds) {
111
+ const timeoutMs = fileTimeoutSeconds * 1000 + 1_000;
112
+ let timeoutHandle = null;
113
+ let timedOut = false;
114
+
115
+ try {
116
+ return await Promise.race([
117
+ subprocess.then((result) => ({ result, timedOut })),
118
+ new Promise((resolve) => {
119
+ timeoutHandle = setTimeout(async () => {
120
+ timedOut = true;
121
+ subprocess.kill("SIGTERM");
122
+ const result = await subprocess.catch((error) => error);
123
+ resolve({ result, timedOut: true });
124
+ }, timeoutMs);
125
+ }),
126
+ ]);
127
+ } finally {
128
+ if (timeoutHandle) clearTimeout(timeoutHandle);
129
+ }
130
+ }
131
+
132
+ export function buildDefaultRuntimeSummaryPath(lease, task) {
133
+ return path.join(lease.leaseDir, "default-runtime", `task-${task.id}.summary.json`);
121
134
  }
122
135
 
123
136
  export function readDefaultRuntimeSummary(filePath) {
@@ -128,6 +141,13 @@ export function readDefaultRuntimeSummary(filePath) {
128
141
  }
129
142
  }
130
143
 
144
+ function defaultFirstLine(output) {
145
+ return String(output || "")
146
+ .split(/\r?\n/)
147
+ .map((line) => line.trim())
148
+ .find(Boolean) || null;
149
+ }
150
+
131
151
  function parseDefaultRuntimeOutput(output) {
132
152
  if (!output) {
133
153
  return {
@@ -1,22 +1,30 @@
1
- export const STACK_MODES = new Set(["shared", "pooled", "isolated"]);
1
+ import {
2
+ DEFAULT_FILE_TIMEOUT_SECONDS,
3
+ normalizeFileTimeoutSeconds,
4
+ parseFileTimeoutOption,
5
+ } from "../shared/file-timeout.mjs";
6
+
7
+ export const DATABASE_BINDINGS = new Set(["shared", "per-runtime"]);
8
+
9
+ export { DEFAULT_FILE_TIMEOUT_SECONDS, parseFileTimeoutOption };
2
10
 
3
11
  export function parseWorkersOption(value) {
4
12
  return parsePositiveInteger(value, "--workers");
5
13
  }
6
14
 
7
- export function parseStackCountOption(value) {
8
- return parsePositiveInteger(value, "--stack-count");
15
+ export function parseRuntimeInstancesOption(value, label = "runtime.instances") {
16
+ return parsePositiveInteger(value, label);
9
17
  }
10
18
 
11
- export function parseStackModeOption(value) {
12
- return normalizeStackModeValue(value, "--stack-mode");
19
+ export function normalizeRuntimeInstances(value, label = "runtime.instances") {
20
+ return normalizePositiveInteger(value, label);
13
21
  }
14
22
 
15
- export function normalizeStackModeValue(value, label = "execution.stackMode") {
23
+ export function normalizeDatabaseBinding(value, label = "database.binding") {
16
24
  const normalized = String(value || "").trim();
17
- if (!STACK_MODES.has(normalized)) {
25
+ if (!DATABASE_BINDINGS.has(normalized)) {
18
26
  throw new Error(
19
- `Invalid ${label} value "${value}". Expected one of: shared, pooled, isolated.`
27
+ `Invalid ${label} value "${value}". Expected one of: shared, per-runtime.`
20
28
  );
21
29
  }
22
30
  return normalized;
@@ -25,70 +33,22 @@ export function normalizeStackModeValue(value, label = "execution.stackMode") {
25
33
  export function resolveExecutionConfig({ cli = {}, repo = {} } = {}) {
26
34
  return normalizeExecutionConfig({
27
35
  workers: cli.workers ?? repo.workers ?? 1,
28
- stackMode: cli.stackMode ?? repo.stackMode ?? "isolated",
29
- stackCount: cli.stackCount ?? repo.stackCount ?? null,
36
+ fileTimeoutSeconds:
37
+ cli.fileTimeoutSeconds ?? repo.fileTimeoutSeconds ?? DEFAULT_FILE_TIMEOUT_SECONDS,
30
38
  });
31
39
  }
32
40
 
33
41
  export function normalizeExecutionConfig(input = {}) {
34
- const workers = normalizePositiveInteger(input.workers, "execution.workers");
35
- const stackMode = normalizeStackMode(input.stackMode);
36
- const explicitStackCount =
37
- input.stackCount == null ? null : normalizePositiveInteger(input.stackCount, "execution.stackCount");
38
-
39
- if (stackMode === "shared") {
40
- if (explicitStackCount !== null && explicitStackCount !== 1) {
41
- throw new Error(`execution.stackCount must be 1 when stackMode is "shared".`);
42
- }
43
- return {
44
- workers,
45
- stackMode,
46
- stackCount: 1,
47
- };
48
- }
49
-
50
- if (stackMode === "pooled") {
51
- return {
52
- workers,
53
- stackMode,
54
- stackCount: explicitStackCount ?? 1,
55
- };
56
- }
57
-
58
- if (explicitStackCount !== null && explicitStackCount !== workers) {
59
- throw new Error(
60
- `execution.stackCount must equal execution.workers when stackMode is "isolated".`
61
- );
62
- }
63
42
  return {
64
- workers,
65
- stackMode,
66
- stackCount: workers,
43
+ workers: normalizePositiveInteger(input.workers ?? 1, "execution.workers"),
44
+ fileTimeoutSeconds: normalizeFileTimeoutSeconds(
45
+ input.fileTimeoutSeconds ?? DEFAULT_FILE_TIMEOUT_SECONDS
46
+ ),
67
47
  };
68
48
  }
69
49
 
70
- export function buildStackIds(execution) {
71
- if (execution.stackMode === "shared") {
72
- return ["shared"];
73
- }
74
-
75
- return Array.from({ length: execution.stackCount }, (_unused, index) => `stack-${index + 1}`);
76
- }
77
-
78
- export function resolveBatchStackMode(defaultMode, suiteMode = null) {
79
- if (suiteMode == null) return defaultMode;
80
- return normalizeStackMode(suiteMode);
81
- }
82
-
83
- export function resolveBatchAccessMode({ framework, type, stackMode }) {
84
- if (stackMode === "isolated") return "exclusive";
85
- if ((framework || "k6") === "playwright") return "exclusive";
86
- if (type === "dal") return "exclusive";
87
- return "shared";
88
- }
89
-
90
- function normalizeStackMode(value) {
91
- return normalizeStackModeValue(value, "execution.stackMode");
50
+ export function buildRuntimeIds(count) {
51
+ return Array.from({ length: count }, (_unused, index) => `runtime-${index + 1}`);
92
52
  }
93
53
 
94
54
  function parsePositiveInteger(value, label) {
@@ -1,101 +1,59 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- buildStackIds,
3
+ buildRuntimeIds,
4
+ DATABASE_BINDINGS,
5
+ DEFAULT_FILE_TIMEOUT_SECONDS,
6
+ normalizeDatabaseBinding,
4
7
  normalizeExecutionConfig,
5
- parseStackCountOption,
6
- parseStackModeOption,
8
+ normalizeRuntimeInstances,
9
+ parseFileTimeoutOption,
10
+ parseRuntimeInstancesOption,
7
11
  parseWorkersOption,
8
- resolveBatchAccessMode,
9
- resolveBatchStackMode,
10
12
  resolveExecutionConfig,
11
13
  } from "./execution-config.mjs";
12
14
 
13
15
  describe("execution-config", () => {
14
- it("parses worker and stack CLI options", () => {
16
+ it("parses worker and runtime-instance options", () => {
15
17
  expect(parseWorkersOption("8")).toBe(8);
16
- expect(parseStackCountOption("2")).toBe(2);
17
- expect(parseStackModeOption("pooled")).toBe("pooled");
18
+ expect(parseRuntimeInstancesOption("2")).toBe(2);
19
+ expect(parseFileTimeoutOption("45")).toBe(45);
18
20
  expect(() => parseWorkersOption("0")).toThrow('Invalid --workers value "0"');
19
- expect(() => parseStackModeOption("legacy")).toThrow('Invalid --stack-mode value "legacy"');
21
+ expect(() => parseRuntimeInstancesOption("0")).toThrow('Invalid runtime.instances value "0"');
22
+ expect(() => parseFileTimeoutOption("0")).toThrow(
23
+ 'Invalid --file-timeout-seconds value "0"'
24
+ );
20
25
  });
21
26
 
22
- it("normalizes shared, pooled, and isolated execution shapes", () => {
23
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "shared" })).toEqual({
27
+ it("normalizes execution defaults", () => {
28
+ expect(normalizeExecutionConfig({ workers: 8 })).toEqual({
24
29
  workers: 8,
25
- stackMode: "shared",
26
- stackCount: 1,
30
+ fileTimeoutSeconds: DEFAULT_FILE_TIMEOUT_SECONDS,
27
31
  });
28
-
29
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "pooled", stackCount: 3 })).toEqual({
30
- workers: 8,
31
- stackMode: "pooled",
32
- stackCount: 3,
33
- });
34
-
35
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "isolated" })).toEqual({
36
- workers: 8,
37
- stackMode: "isolated",
38
- stackCount: 8,
39
- });
40
- });
41
-
42
- it("rejects invalid stack-count combinations", () => {
43
- expect(() =>
44
- normalizeExecutionConfig({ workers: 8, stackMode: "shared", stackCount: 2 })
45
- ).toThrow('execution.stackCount must be 1 when stackMode is "shared"');
46
-
47
- expect(() =>
48
- normalizeExecutionConfig({ workers: 8, stackMode: "isolated", stackCount: 2 })
49
- ).toThrow('execution.stackCount must equal execution.workers when stackMode is "isolated"');
50
32
  });
51
33
 
52
34
  it("applies CLI values over repo execution defaults", () => {
53
35
  expect(
54
36
  resolveExecutionConfig({
55
- repo: { workers: 3, stackMode: "shared" },
56
- cli: { workers: 6, stackMode: "pooled", stackCount: 2 },
37
+ repo: { workers: 3, fileTimeoutSeconds: 30 },
38
+ cli: { workers: 6, fileTimeoutSeconds: 90 },
57
39
  })
58
40
  ).toEqual({
59
41
  workers: 6,
60
- stackMode: "pooled",
61
- stackCount: 2,
42
+ fileTimeoutSeconds: 90,
62
43
  });
63
44
  });
64
45
 
65
- it("builds stack ids from the normalized execution shape", () => {
66
- expect(buildStackIds({ stackMode: "shared", stackCount: 1 })).toEqual(["shared"]);
67
- expect(buildStackIds({ stackMode: "pooled", stackCount: 2 })).toEqual([
68
- "stack-1",
69
- "stack-2",
70
- ]);
46
+ it("builds runtime ids from an instance count", () => {
47
+ expect(buildRuntimeIds(3)).toEqual(["runtime-1", "runtime-2", "runtime-3"]);
71
48
  });
72
49
 
73
- it("resolves batch stack and access modes", () => {
74
- expect(resolveBatchStackMode("shared", null)).toBe("shared");
75
- expect(resolveBatchStackMode("shared", "isolated")).toBe("isolated");
76
-
77
- expect(
78
- resolveBatchAccessMode({
79
- framework: "k6",
80
- type: "integration",
81
- stackMode: "shared",
82
- })
83
- ).toBe("shared");
84
-
85
- expect(
86
- resolveBatchAccessMode({
87
- framework: "playwright",
88
- type: "e2e",
89
- stackMode: "pooled",
90
- })
91
- ).toBe("exclusive");
92
-
93
- expect(
94
- resolveBatchAccessMode({
95
- framework: "k6",
96
- type: "dal",
97
- stackMode: "shared",
98
- })
99
- ).toBe("exclusive");
50
+ it("normalizes runtime instances and database bindings", () => {
51
+ expect(normalizeRuntimeInstances(2)).toBe(2);
52
+ for (const binding of DATABASE_BINDINGS) {
53
+ expect(normalizeDatabaseBinding(binding)).toBe(binding);
54
+ }
55
+ expect(() => normalizeDatabaseBinding("legacy")).toThrow(
56
+ 'Invalid database.binding value "legacy"'
57
+ );
100
58
  });
101
59
  });
@@ -36,21 +36,6 @@ export function longestServiceName(results) {
36
36
  return results.reduce((max, result) => Math.max(max, result.name.length), 4);
37
37
  }
38
38
 
39
- export function formatBatchDescriptor(batch) {
40
- const fileLabel = `${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}`;
41
- const frameworkLabel = formatFrameworkLabel(batch.framework);
42
- return frameworkLabel ? ` (${frameworkLabel}, ${fileLabel})` : ` (${fileLabel})`;
43
- }
44
-
45
- export function formatPlaywrightBatchFiles(batch) {
46
- if (!batch?.tasks?.length) return "";
47
- const files = batch.tasks.map((task) => task.file);
48
- if (files.length === 1) return ` · ${files[0]}`;
49
- const preview = files.slice(0, 3).join(", ");
50
- const suffix = files.length > 3 ? `, +${files.length - 3} more` : "";
51
- return ` · ${preview}${suffix}`;
52
- }
53
-
54
39
  export function formatFrameworkLabel(framework) {
55
40
  if (!framework || framework === "k6") return "";
56
41
  return framework;
@@ -1,11 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  buildRunSummaryLines,
4
- formatBatchDescriptor,
5
4
  formatDuration,
6
5
  formatError,
7
6
  formatFrameworkLabel,
8
- formatPlaywrightBatchFiles,
9
7
  formatServiceSummary,
10
8
  formatSuiteFramework,
11
9
  longestServiceName,
@@ -53,22 +51,6 @@ describe("runner formatting", () => {
53
51
  ).toBe("1/2 suites passed, 1/3 files passed, 1 suite skipped");
54
52
  });
55
53
 
56
- it("formats batch descriptors", () => {
57
- expect(formatBatchDescriptor({ framework: "k6", tasks: [{}, {}] })).toBe(" (2 files)");
58
- expect(formatBatchDescriptor({ framework: "playwright", tasks: [{}] })).toBe(
59
- " (playwright, 1 file)"
60
- );
61
- });
62
-
63
- it("formats Playwright file previews", () => {
64
- expect(formatPlaywrightBatchFiles({ tasks: [{ file: "a" }] })).toBe(" · a");
65
- expect(
66
- formatPlaywrightBatchFiles({
67
- tasks: [{ file: "a" }, { file: "b" }, { file: "c" }, { file: "d" }],
68
- })
69
- ).toBe(" · a, b, c, +1 more");
70
- });
71
-
72
54
  it("formats framework labels", () => {
73
55
  expect(formatFrameworkLabel("k6")).toBe("");
74
56
  expect(formatFrameworkLabel("playwright")).toBe("playwright");
@@ -21,7 +21,7 @@ export function createRunLifecycle(productDir) {
21
21
  interruptReason: null,
22
22
  services: [],
23
23
  graphDirs: [],
24
- stackStateDirs: [],
24
+ runtimeDirs: [],
25
25
  runtimeStateDirs: [],
26
26
  };
27
27
  const signalListeners = [];
@@ -66,7 +66,7 @@ export function createRunLifecycle(productDir) {
66
66
  trackGraphContext(context) {
67
67
  mutate((draft) => {
68
68
  pushUnique(draft.graphDirs, context.graphDir);
69
- pushUnique(draft.stackStateDirs, context.stackStateDir);
69
+ pushUnique(draft.runtimeDirs, context.runtimeDir);
70
70
  for (const runtimeConfig of context.runtimeConfigs || []) {
71
71
  if (runtimeConfig.stateDir) pushUnique(draft.runtimeStateDirs, runtimeConfig.stateDir);
72
72
  }
@@ -78,7 +78,7 @@ export function createRunLifecycle(productDir) {
78
78
  draft.services = draft.services.filter((service) => service.pid !== child.pid);
79
79
  draft.services.push({
80
80
  serviceName: config.name,
81
- stackLabel: config.stackLabel,
81
+ runtimeLabel: config.runtimeLabel,
82
82
  command: config.testkit.local?.start || null,
83
83
  cwd,
84
84
  pid: child.pid,
@@ -195,8 +195,8 @@ export function findPortOwner(productDir, { host, port }) {
195
195
  }
196
196
 
197
197
  export function formatRunSummary(manifest) {
198
- const stackLabels = [...new Set((manifest.services || []).map((service) => service.stackLabel).filter(Boolean))];
199
- return `${manifest.runId} pid=${manifest.pid}${stackLabels.length > 0 ? ` stacks=${stackLabels.join(",")}` : ""}`;
198
+ const runtimeLabels = [...new Set((manifest.services || []).map((service) => service.runtimeLabel).filter(Boolean))];
199
+ return `${manifest.runId} pid=${manifest.pid}${runtimeLabels.length > 0 ? ` runtimes=${runtimeLabels.join(",")}` : ""}`;
200
200
  }
201
201
 
202
202
  export function isPidRunning(pid) {
@@ -220,8 +220,8 @@ async function cleanupRunManifest(productDir, manifest, { removeRuntimeState = f
220
220
  await destroyRuntimeDatabase({ productDir, stateDir });
221
221
  }
222
222
 
223
- for (const stackStateDir of [...new Set(manifest.stackStateDirs || [])].sort((a, b) => b.length - a.length)) {
224
- fs.rmSync(stackStateDir, { recursive: true, force: true });
223
+ for (const runtimeDir of [...new Set(manifest.runtimeDirs || [])].sort((a, b) => b.length - a.length)) {
224
+ fs.rmSync(runtimeDir, { recursive: true, force: true });
225
225
  }
226
226
 
227
227
  for (const graphDir of [...new Set(manifest.graphDirs || [])].sort((a, b) => b.length - a.length)) {
@@ -2,7 +2,7 @@ import {
2
2
  applyShard,
3
3
  buildRuntimeGraphs,
4
4
  buildTaskQueue,
5
- claimNextBatch,
5
+ claimNextTask,
6
6
  collectSuites,
7
7
  resolveRuntimeConfigs,
8
8
  } from "./planning.mjs";
@@ -36,7 +36,7 @@ import {
36
36
  safeUsername,
37
37
  } from "./metadata.mjs";
38
38
  import { resolveExecutionConfig } from "./execution-config.mjs";
39
- import { createStackManager } from "./stack-manager.mjs";
39
+ import { createRuntimeManager } from "./runtime-manager.mjs";
40
40
  import { createWorker, runWorker } from "./worker-loop.mjs";
41
41
  import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
42
42
  import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
@@ -105,7 +105,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
105
105
  const trackers = buildServiceTrackers(servicePlans, startedAt);
106
106
  const executedPlans = servicePlans.filter((plan) => !plan.skipped);
107
107
  let workerCount = 0;
108
- let stackCount = 0;
108
+ let runtimeInstanceCount = 0;
109
109
  let exitCode = 0;
110
110
  const lifecycle = createRunLifecycle(productDir);
111
111
  lifecycle.markRunning();
@@ -119,14 +119,13 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
119
119
  const graphs = buildRuntimeGraphs(executedPlans);
120
120
  const queue = buildTaskQueue(executedPlans, graphs, timings);
121
121
  workerCount = Math.max(1, Math.min(execution.workers, queue.length));
122
- stackCount = execution.stackMode === "shared" ? 1 : execution.stackCount;
122
+ runtimeInstanceCount = graphs.reduce((sum, graph) => sum + graph.instanceCount, 0);
123
123
  const workers = Array.from({ length: workerCount }, (_unused, index) =>
124
124
  createWorker(index + 1, productDir)
125
125
  );
126
- const stackManager = createStackManager({
126
+ const runtimeManager = createRuntimeManager({
127
127
  productDir,
128
128
  graphs,
129
- execution,
130
129
  lifecycle,
131
130
  });
132
131
  const timingUpdates = [];
@@ -137,11 +136,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
137
136
  runWorker(
138
137
  worker,
139
138
  queue,
140
- stackManager,
139
+ runtimeManager,
141
140
  trackers,
142
141
  timingUpdates,
143
142
  lifecycle,
144
- claimNextBatch,
143
+ claimNextTask,
145
144
  recordTaskOutcome,
146
145
  recordGraphError
147
146
  )
@@ -157,7 +156,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
157
156
  }
158
157
  }
159
158
  } finally {
160
- await stackManager.cleanupAll();
159
+ await runtimeManager.cleanupAll();
161
160
  }
162
161
 
163
162
  saveTimings(productDir, timings, timingUpdates);
@@ -174,7 +173,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
174
173
  finishedAt,
175
174
  execution,
176
175
  workerCount,
177
- stackCount,
176
+ runtimeInstanceCount,
178
177
  typeValues,
179
178
  suiteSelectors,
180
179
  fileNames: requestedFiles,