@elench/testkit 0.1.40 → 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 (37) hide show
  1. package/README.md +25 -13
  2. package/lib/cli/args.mjs +0 -4
  3. package/lib/cli/args.test.mjs +0 -5
  4. package/lib/cli/index.mjs +0 -9
  5. package/lib/config/index.mjs +67 -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 +31 -53
  10. package/lib/runner/execution-config.mjs +14 -70
  11. package/lib/runner/execution-config.test.mjs +22 -74
  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 +2 -3
  23. package/lib/runner/reporting.test.mjs +2 -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/setup/index.d.ts +14 -10
  36. package/package.json +1 -1
  37. package/lib/runner/stack-manager.mjs +0 -146
@@ -4,7 +4,7 @@ import {
4
4
  parseFileTimeoutOption,
5
5
  } from "../shared/file-timeout.mjs";
6
6
 
7
- export const STACK_MODES = new Set(["shared", "pooled", "isolated"]);
7
+ export const DATABASE_BINDINGS = new Set(["shared", "per-runtime"]);
8
8
 
9
9
  export { DEFAULT_FILE_TIMEOUT_SECONDS, parseFileTimeoutOption };
10
10
 
@@ -12,19 +12,19 @@ export function parseWorkersOption(value) {
12
12
  return parsePositiveInteger(value, "--workers");
13
13
  }
14
14
 
15
- export function parseStackCountOption(value) {
16
- return parsePositiveInteger(value, "--stack-count");
15
+ export function parseRuntimeInstancesOption(value, label = "runtime.instances") {
16
+ return parsePositiveInteger(value, label);
17
17
  }
18
18
 
19
- export function parseStackModeOption(value) {
20
- return normalizeStackModeValue(value, "--stack-mode");
19
+ export function normalizeRuntimeInstances(value, label = "runtime.instances") {
20
+ return normalizePositiveInteger(value, label);
21
21
  }
22
22
 
23
- export function normalizeStackModeValue(value, label = "execution.stackMode") {
23
+ export function normalizeDatabaseBinding(value, label = "database.binding") {
24
24
  const normalized = String(value || "").trim();
25
- if (!STACK_MODES.has(normalized)) {
25
+ if (!DATABASE_BINDINGS.has(normalized)) {
26
26
  throw new Error(
27
- `Invalid ${label} value "${value}". Expected one of: shared, pooled, isolated.`
27
+ `Invalid ${label} value "${value}". Expected one of: shared, per-runtime.`
28
28
  );
29
29
  }
30
30
  return normalized;
@@ -33,78 +33,22 @@ export function normalizeStackModeValue(value, label = "execution.stackMode") {
33
33
  export function resolveExecutionConfig({ cli = {}, repo = {} } = {}) {
34
34
  return normalizeExecutionConfig({
35
35
  workers: cli.workers ?? repo.workers ?? 1,
36
- stackMode: cli.stackMode ?? repo.stackMode ?? "isolated",
37
- stackCount: cli.stackCount ?? repo.stackCount ?? null,
38
36
  fileTimeoutSeconds:
39
37
  cli.fileTimeoutSeconds ?? repo.fileTimeoutSeconds ?? DEFAULT_FILE_TIMEOUT_SECONDS,
40
38
  });
41
39
  }
42
40
 
43
41
  export function normalizeExecutionConfig(input = {}) {
44
- const workers = normalizePositiveInteger(input.workers, "execution.workers");
45
- const stackMode = normalizeStackMode(input.stackMode);
46
- const explicitStackCount =
47
- input.stackCount == null ? null : normalizePositiveInteger(input.stackCount, "execution.stackCount");
48
- const fileTimeoutSeconds = normalizeFileTimeoutSeconds(
49
- input.fileTimeoutSeconds ?? DEFAULT_FILE_TIMEOUT_SECONDS
50
- );
51
-
52
- if (stackMode === "shared") {
53
- if (explicitStackCount !== null && explicitStackCount !== 1) {
54
- throw new Error(`execution.stackCount must be 1 when stackMode is "shared".`);
55
- }
56
- return {
57
- workers,
58
- stackMode,
59
- stackCount: 1,
60
- fileTimeoutSeconds,
61
- };
62
- }
63
-
64
- if (stackMode === "pooled") {
65
- return {
66
- workers,
67
- stackMode,
68
- stackCount: explicitStackCount ?? 1,
69
- fileTimeoutSeconds,
70
- };
71
- }
72
-
73
- if (explicitStackCount !== null && explicitStackCount !== workers) {
74
- throw new Error(
75
- `execution.stackCount must equal execution.workers when stackMode is "isolated".`
76
- );
77
- }
78
42
  return {
79
- workers,
80
- stackMode,
81
- stackCount: workers,
82
- fileTimeoutSeconds,
43
+ workers: normalizePositiveInteger(input.workers ?? 1, "execution.workers"),
44
+ fileTimeoutSeconds: normalizeFileTimeoutSeconds(
45
+ input.fileTimeoutSeconds ?? DEFAULT_FILE_TIMEOUT_SECONDS
46
+ ),
83
47
  };
84
48
  }
85
49
 
86
- export function buildStackIds(execution) {
87
- if (execution.stackMode === "shared") {
88
- return ["shared"];
89
- }
90
-
91
- return Array.from({ length: execution.stackCount }, (_unused, index) => `stack-${index + 1}`);
92
- }
93
-
94
- export function resolveBatchStackMode(defaultMode, suiteMode = null) {
95
- if (suiteMode == null) return defaultMode;
96
- return normalizeStackMode(suiteMode);
97
- }
98
-
99
- export function resolveBatchAccessMode({ framework, type, stackMode }) {
100
- if (stackMode === "isolated") return "exclusive";
101
- if ((framework || "k6") === "playwright") return "exclusive";
102
- if (type === "dal") return "exclusive";
103
- return "shared";
104
- }
105
-
106
- function normalizeStackMode(value) {
107
- return normalizeStackModeValue(value, "execution.stackMode");
50
+ export function buildRuntimeIds(count) {
51
+ return Array.from({ length: count }, (_unused, index) => `runtime-${index + 1}`);
108
52
  }
109
53
 
110
54
  function parsePositiveInteger(value, label) {
@@ -1,111 +1,59 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- buildStackIds,
3
+ buildRuntimeIds,
4
+ DATABASE_BINDINGS,
4
5
  DEFAULT_FILE_TIMEOUT_SECONDS,
6
+ normalizeDatabaseBinding,
5
7
  normalizeExecutionConfig,
8
+ normalizeRuntimeInstances,
6
9
  parseFileTimeoutOption,
7
- parseStackCountOption,
8
- parseStackModeOption,
10
+ parseRuntimeInstancesOption,
9
11
  parseWorkersOption,
10
- resolveBatchAccessMode,
11
- resolveBatchStackMode,
12
12
  resolveExecutionConfig,
13
13
  } from "./execution-config.mjs";
14
14
 
15
15
  describe("execution-config", () => {
16
- it("parses worker and stack CLI options", () => {
16
+ it("parses worker and runtime-instance options", () => {
17
17
  expect(parseWorkersOption("8")).toBe(8);
18
+ expect(parseRuntimeInstancesOption("2")).toBe(2);
18
19
  expect(parseFileTimeoutOption("45")).toBe(45);
19
- expect(parseStackCountOption("2")).toBe(2);
20
- expect(parseStackModeOption("pooled")).toBe("pooled");
21
20
  expect(() => parseWorkersOption("0")).toThrow('Invalid --workers value "0"');
21
+ expect(() => parseRuntimeInstancesOption("0")).toThrow('Invalid runtime.instances value "0"');
22
22
  expect(() => parseFileTimeoutOption("0")).toThrow(
23
23
  'Invalid --file-timeout-seconds value "0"'
24
24
  );
25
- expect(() => parseStackModeOption("legacy")).toThrow('Invalid --stack-mode value "legacy"');
26
25
  });
27
26
 
28
- it("normalizes shared, pooled, and isolated execution shapes", () => {
29
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "shared" })).toEqual({
27
+ it("normalizes execution defaults", () => {
28
+ expect(normalizeExecutionConfig({ workers: 8 })).toEqual({
30
29
  workers: 8,
31
30
  fileTimeoutSeconds: DEFAULT_FILE_TIMEOUT_SECONDS,
32
- stackMode: "shared",
33
- stackCount: 1,
34
31
  });
35
-
36
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "pooled", stackCount: 3 })).toEqual({
37
- workers: 8,
38
- fileTimeoutSeconds: DEFAULT_FILE_TIMEOUT_SECONDS,
39
- stackMode: "pooled",
40
- stackCount: 3,
41
- });
42
-
43
- expect(normalizeExecutionConfig({ workers: 8, stackMode: "isolated" })).toEqual({
44
- workers: 8,
45
- fileTimeoutSeconds: DEFAULT_FILE_TIMEOUT_SECONDS,
46
- stackMode: "isolated",
47
- stackCount: 8,
48
- });
49
- });
50
-
51
- it("rejects invalid stack-count combinations", () => {
52
- expect(() =>
53
- normalizeExecutionConfig({ workers: 8, stackMode: "shared", stackCount: 2 })
54
- ).toThrow('execution.stackCount must be 1 when stackMode is "shared"');
55
-
56
- expect(() =>
57
- normalizeExecutionConfig({ workers: 8, stackMode: "isolated", stackCount: 2 })
58
- ).toThrow('execution.stackCount must equal execution.workers when stackMode is "isolated"');
59
32
  });
60
33
 
61
34
  it("applies CLI values over repo execution defaults", () => {
62
35
  expect(
63
36
  resolveExecutionConfig({
64
- repo: { workers: 3, stackMode: "shared" },
65
- cli: { workers: 6, fileTimeoutSeconds: 90, stackMode: "pooled", stackCount: 2 },
37
+ repo: { workers: 3, fileTimeoutSeconds: 30 },
38
+ cli: { workers: 6, fileTimeoutSeconds: 90 },
66
39
  })
67
40
  ).toEqual({
68
41
  workers: 6,
69
42
  fileTimeoutSeconds: 90,
70
- stackMode: "pooled",
71
- stackCount: 2,
72
43
  });
73
44
  });
74
45
 
75
- it("builds stack ids from the normalized execution shape", () => {
76
- expect(buildStackIds({ stackMode: "shared", stackCount: 1 })).toEqual(["shared"]);
77
- expect(buildStackIds({ stackMode: "pooled", stackCount: 2 })).toEqual([
78
- "stack-1",
79
- "stack-2",
80
- ]);
46
+ it("builds runtime ids from an instance count", () => {
47
+ expect(buildRuntimeIds(3)).toEqual(["runtime-1", "runtime-2", "runtime-3"]);
81
48
  });
82
49
 
83
- it("resolves batch stack and access modes", () => {
84
- expect(resolveBatchStackMode("shared", null)).toBe("shared");
85
- expect(resolveBatchStackMode("shared", "isolated")).toBe("isolated");
86
-
87
- expect(
88
- resolveBatchAccessMode({
89
- framework: "k6",
90
- type: "integration",
91
- stackMode: "shared",
92
- })
93
- ).toBe("shared");
94
-
95
- expect(
96
- resolveBatchAccessMode({
97
- framework: "playwright",
98
- type: "e2e",
99
- stackMode: "pooled",
100
- })
101
- ).toBe("exclusive");
102
-
103
- expect(
104
- resolveBatchAccessMode({
105
- framework: "k6",
106
- type: "dal",
107
- stackMode: "shared",
108
- })
109
- ).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
+ );
110
58
  });
111
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,