@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
@@ -36,12 +36,12 @@ export async function startLocalService(config, lifecycle) {
36
36
 
37
37
  await assertLocalServicePortsAvailable(config, isPortInUse);
38
38
 
39
- console.log(`Starting ${config.stackLabel}:${config.name}: ${config.testkit.local.start}`);
39
+ console.log(`Starting ${config.runtimeLabel}:${config.name}: ${config.testkit.local.start}`);
40
40
  const child = startDetachedCommand(config.testkit.local.start, cwd, env);
41
41
 
42
42
  const outputDrains = [
43
- pipeOutput(child.stdout, `[${config.stackLabel}:${config.name}]`),
44
- pipeOutput(child.stderr, `[${config.stackLabel}:${config.name}]`),
43
+ pipeOutput(child.stdout, `[${config.runtimeLabel}:${config.name}]`),
44
+ pipeOutput(child.stderr, `[${config.runtimeLabel}:${config.name}]`),
45
45
  ];
46
46
  lifecycle.registerService(config, child, cwd);
47
47
 
@@ -49,7 +49,7 @@ export async function startLocalService(config, lifecycle) {
49
49
 
50
50
  try {
51
51
  await waitForReady({
52
- name: `${config.stackLabel}:${config.name}`,
52
+ name: `${config.runtimeLabel}:${config.name}`,
53
53
  url: config.testkit.local.readyUrl,
54
54
  timeoutMs: readyTimeoutMs,
55
55
  process: child,
@@ -46,8 +46,7 @@ export function writeGraphMetadata(graphDir, graph) {
46
46
  fs.mkdirSync(graphDir, { recursive: true });
47
47
  const metadata = {
48
48
  runtimeServices: graph.runtimeNames,
49
- assignedTargets: [...graph.assignedTargets].sort(),
50
- rootService: graph.rootConfig.name,
49
+ targetServices: [...(graph.targetNames || [])].sort(),
51
50
  };
52
51
  fs.writeFileSync(
53
52
  path.join(graphDir, GRAPH_METADATA),
@@ -48,14 +48,12 @@ describe("runner-state", () => {
48
48
  const graphDir = path.join(productDir, ".testkit", "_graphs", "api__frontend");
49
49
  writeGraphMetadata(graphDir, {
50
50
  runtimeNames: ["api", "frontend"],
51
- assignedTargets: ["frontend", "api"],
52
- rootConfig: { name: "api" },
51
+ targetNames: ["frontend", "api"],
53
52
  });
54
53
 
55
54
  expect(readGraphMetadata(graphDir)).toEqual({
56
55
  runtimeServices: ["api", "frontend"],
57
- assignedTargets: ["api", "frontend"],
58
- rootService: "api",
56
+ targetServices: ["api", "frontend"],
59
57
  });
60
58
 
61
59
  expect(findGraphDirsForService(productDir, "frontend")).toEqual([graphDir]);
@@ -3,26 +3,28 @@ import { readDatabaseInfo } from "./state-io.mjs";
3
3
 
4
4
  const PORT_STRIDE = 100;
5
5
 
6
- export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId, stackStateDir) {
7
- const portMap = buildPortMap(runtimeConfigs, stackId);
6
+ export function resolveRuntimeInstanceConfigs(runtimeConfigs, runtimeId, runtimeDir, options = {}) {
7
+ const runtimeLabel = buildRuntimeLabel(options.graphDirName, runtimeId);
8
+ const portMap = buildPortMap(runtimeConfigs, runtimeId, {
9
+ index: options.portNamespaceIndex || 0,
10
+ stride: options.portNamespaceStride || 1,
11
+ });
8
12
  const baseUrlByService = new Map();
9
13
  const readyUrlByService = new Map();
10
14
  const stateDirByService = new Map();
11
15
 
12
16
  for (const config of runtimeConfigs) {
13
- stateDirByService.set(
14
- config.name,
15
- resolveServiceStateDir(stackStateDir, targetConfig.name, config)
16
- );
17
+ stateDirByService.set(config.name, resolveServiceStateDir(runtimeDir, config));
17
18
  }
18
19
 
19
20
  for (const config of runtimeConfigs) {
20
21
  if (!config.testkit.local) continue;
21
22
  baseUrlByService.set(
22
23
  config.name,
23
- resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, targetConfig, stackId, {
24
- stackId,
25
- stackStateDir,
24
+ resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, runtimeId, {
25
+ runtimeId,
26
+ runtimeLabel,
27
+ runtimeDir,
26
28
  portMap,
27
29
  baseUrlByService,
28
30
  readyUrlByService,
@@ -31,9 +33,10 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
31
33
  );
32
34
  readyUrlByService.set(
33
35
  config.name,
34
- resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, targetConfig, stackId, {
35
- stackId,
36
- stackStateDir,
36
+ resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, runtimeId, {
37
+ runtimeId,
38
+ runtimeLabel,
39
+ runtimeDir,
37
40
  portMap,
38
41
  baseUrlByService,
39
42
  readyUrlByService,
@@ -47,20 +50,16 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
47
50
  if (!config.testkit.local) continue;
48
51
  const resolvedBaseUrl = baseUrlByService.get(config.name);
49
52
  const resolvedReadyUrl = readyUrlByService.get(config.name);
50
- if (resolvedBaseUrl) {
51
- urlMappings.push([config.testkit.local.baseUrl, resolvedBaseUrl]);
52
- }
53
- if (resolvedReadyUrl) {
54
- urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
55
- }
53
+ if (resolvedBaseUrl) urlMappings.push([config.testkit.local.baseUrl, resolvedBaseUrl]);
54
+ if (resolvedReadyUrl) urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
56
55
  }
57
56
 
58
57
  return runtimeConfigs.map((config) =>
59
- resolveStackConfig(
58
+ resolveRuntimeConfig(
60
59
  config,
61
- targetConfig,
62
- stackId,
63
- stackStateDir,
60
+ runtimeId,
61
+ runtimeLabel,
62
+ runtimeDir,
64
63
  portMap,
65
64
  baseUrlByService,
66
65
  readyUrlByService,
@@ -70,10 +69,10 @@ export function resolveStackRuntimeConfigs(targetConfig, runtimeConfigs, stackId
70
69
  );
71
70
  }
72
71
 
73
- export function buildPortMap(runtimeConfigs, stackId) {
72
+ export function buildPortMap(runtimeConfigs, runtimeId, namespace = {}) {
74
73
  const portMap = new Map();
75
74
  const seen = new Map();
76
- const offset = stackPortOffset(stackId);
75
+ const offset = runtimePortOffset(runtimeId, namespace);
77
76
 
78
77
  for (const config of runtimeConfigs) {
79
78
  if (!config.testkit.local) continue;
@@ -82,10 +81,16 @@ export function buildPortMap(runtimeConfigs, stackId) {
82
81
  if (!basePort) continue;
83
82
 
84
83
  const actualPort = basePort + offset;
84
+ if (actualPort > 65_535) {
85
+ throw new Error(
86
+ `Runtime port resolution exceeded 65535 for service "${config.name}" (${actualPort}). ` +
87
+ `Reduce runtime instances or choose lower local.port/baseUrl ports in testkit.setup.ts.`
88
+ );
89
+ }
85
90
  const existing = seen.get(actualPort);
86
91
  if (existing) {
87
92
  throw new Error(
88
- `Stack port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
93
+ `Runtime port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
89
94
  `Assign distinct local.port/baseUrl ports in testkit.setup.ts.`
90
95
  );
91
96
  }
@@ -96,21 +101,22 @@ export function buildPortMap(runtimeConfigs, stackId) {
96
101
  return portMap;
97
102
  }
98
103
 
99
- export function resolveStackConfig(
104
+ export function resolveRuntimeConfig(
100
105
  config,
101
- targetConfig,
102
- stackId,
103
- stackStateDir,
106
+ runtimeId,
107
+ runtimeLabel,
108
+ runtimeDir,
104
109
  portMap,
105
110
  baseUrlByService,
106
111
  readyUrlByService,
107
112
  stateDirByService,
108
113
  urlMappings
109
114
  ) {
110
- const stateDir = resolveServiceStateDir(stackStateDir, targetConfig.name, config);
115
+ const stateDir = resolveServiceStateDir(runtimeDir, config);
111
116
  const context = {
112
- stackId,
113
- targetName: targetConfig.name,
117
+ runtimeId,
118
+ runtimeLabel,
119
+ runtimeDir,
114
120
  serviceName: config.name,
115
121
  serviceStateDir: stateDir,
116
122
  portMap,
@@ -118,6 +124,8 @@ export function resolveStackConfig(
118
124
  readyUrlByService,
119
125
  stateDirByService,
120
126
  urlMappings,
127
+ leaseId: null,
128
+ leaseDir: null,
121
129
  };
122
130
 
123
131
  const database = config.testkit.database
@@ -166,9 +174,8 @@ export function resolveStackConfig(
166
174
  return {
167
175
  ...config,
168
176
  stateDir,
169
- stackId,
170
- stackLabel: stackId,
171
- targetName: targetConfig.name,
177
+ runtimeId,
178
+ runtimeLabel,
172
179
  testkit: {
173
180
  ...config.testkit,
174
181
  database,
@@ -180,52 +187,66 @@ export function resolveStackConfig(
180
187
  };
181
188
  }
182
189
 
183
- export function resolveServiceStateDir(stackStateDir, targetName, config) {
184
- const dbSource = config.testkit.databaseFrom || config.name;
185
- return getStackServiceStateDir(stackStateDir, targetName, dbSource);
190
+ export function resolveServiceStateDir(runtimeDir, config) {
191
+ return path.join(runtimeDir, "services", config.name);
186
192
  }
187
193
 
188
- export function getStackServiceStateDir(stackStateDir, targetName, serviceName) {
189
- if (targetName === serviceName) {
190
- return stackStateDir;
191
- }
192
- return path.join(stackStateDir, "deps", serviceName);
194
+ export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
195
+ return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
193
196
  }
194
197
 
195
- export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
198
+ export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
199
+ return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
200
+ }
201
+
202
+ function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
196
203
  const inheritedEnv = { ...processEnv };
197
- const templateContext = config.testkit?.templateContext;
204
+ const templateContext = buildTemplateContext(config, lease);
198
205
  const env = {
199
206
  ...inheritedEnv,
200
207
  ...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
201
208
  ...resolveEnvTemplates(extraEnv, templateContext),
202
209
  TESTKIT_ACTIVE: "1",
203
- ...(config.stackId ? { TESTKIT_STACK_ID: String(config.stackId) } : {}),
210
+ ...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
211
+ ...(lease?.leaseId ? { TESTKIT_LEASE_ID: String(lease.leaseId) } : {}),
212
+ ...(lease?.leaseDir ? { TESTKIT_LEASE_DIR: lease.leaseDir } : {}),
204
213
  };
205
214
  delete env.DATABASE_URL;
206
215
  return env;
207
216
  }
208
217
 
209
- export function buildPlaywrightEnv(config, baseUrl, processEnv = process.env) {
210
- return buildExecutionEnv(
218
+ export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.env) {
219
+ return buildTaskExecutionEnv(
211
220
  config,
221
+ lease,
212
222
  {
213
223
  BASE_URL: baseUrl,
214
224
  PLAYWRIGHT_HTML_OPEN: "never",
215
225
  PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
216
226
  processEnv.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
217
227
  TESTKIT_MANAGED_SERVERS: "1",
218
- TESTKIT_STACK_ID: String(config.stackId),
219
228
  },
220
229
  processEnv
221
230
  );
222
231
  }
223
232
 
224
- export function resolveRuntimeUrl(rawUrl, serviceName, targetConfig, stackId, context) {
233
+ function buildTemplateContext(config, lease) {
234
+ const baseContext = config.testkit?.templateContext || {};
235
+ return {
236
+ ...baseContext,
237
+ runtimeId: config.runtimeId || baseContext.runtimeId || null,
238
+ runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
239
+ serviceName: config.name,
240
+ serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
241
+ leaseId: lease?.leaseId || null,
242
+ leaseDir: lease?.leaseDir || null,
243
+ };
244
+ }
245
+
246
+ export function resolveRuntimeUrl(rawUrl, serviceName, runtimeId, context) {
225
247
  const resolved = resolveTemplateString(rawUrl, {
226
248
  ...context,
227
- targetName: targetConfig.name,
228
- stackId,
249
+ runtimeId,
229
250
  serviceName,
230
251
  });
231
252
  const actualPort = context.portMap.get(serviceName);
@@ -247,14 +268,16 @@ export function resolveTemplateString(value, context) {
247
268
 
248
269
  return value.replace(/\{([a-zA-Z]+)(?::([a-zA-Z0-9_-]+))?\}/g, (_match, token, arg) => {
249
270
  switch (token) {
250
- case "stack":
251
- return String(context.stackId);
252
- case "target":
253
- return context.targetName;
271
+ case "runtime":
272
+ return String(context.runtimeId);
254
273
  case "service":
255
274
  return context.serviceName;
256
275
  case "stateDir":
257
276
  return context.serviceStateDir;
277
+ case "lease":
278
+ return context.leaseId ? String(context.leaseId) : "";
279
+ case "leaseDir":
280
+ return context.leaseDir || "";
258
281
  case "port": {
259
282
  const serviceName = arg || context.serviceName;
260
283
  const port = context.portMap.get(serviceName);
@@ -381,8 +404,15 @@ export function normalizeSocketHost(hostname) {
381
404
  return hostname;
382
405
  }
383
406
 
384
- function stackPortOffset(stackId) {
385
- const match = String(stackId).match(/(\d+)$/);
386
- if (!match) return 0;
387
- return PORT_STRIDE * (Number.parseInt(match[1], 10) - 1);
407
+ function runtimePortOffset(runtimeId, namespace = {}) {
408
+ const match = String(runtimeId).match(/(\d+)$/);
409
+ const runtimeIndex = match ? Number.parseInt(match[1], 10) - 1 : 0;
410
+ const namespaceIndex = Math.max(0, Number(namespace.index || 0));
411
+ const namespaceStride = Math.max(1, Number(namespace.stride || 1));
412
+ return PORT_STRIDE * (namespaceIndex * namespaceStride + runtimeIndex);
413
+ }
414
+
415
+ function buildRuntimeLabel(graphDirName, runtimeId) {
416
+ if (!graphDirName) return runtimeId;
417
+ return `${graphDirName}/${runtimeId}`;
388
418
  }
@@ -6,12 +6,12 @@ import {
6
6
  buildExecutionEnv,
7
7
  buildPlaywrightEnv,
8
8
  buildPortMap,
9
+ buildTaskExecutionEnv,
9
10
  finalizeString,
10
- getStackServiceStateDir,
11
11
  normalizeSocketHost,
12
12
  numericPortFromUrl,
13
+ resolveRuntimeInstanceConfigs,
13
14
  resolveServiceStateDir,
14
- resolveStackRuntimeConfigs,
15
15
  resolveTemplateString,
16
16
  rewriteUrlPort,
17
17
  socketFromUrl,
@@ -20,12 +20,15 @@ import {
20
20
  function makeRuntimeConfig(name, local, extras = {}) {
21
21
  return {
22
22
  name,
23
+ stateDir: extras.stateDir,
24
+ runtimeId: extras.runtimeId,
23
25
  testkit: {
24
26
  local,
25
27
  serviceEnv: extras.serviceEnv || {},
26
28
  databaseFrom: extras.databaseFrom,
27
29
  migrate: extras.migrate,
28
30
  seed: extras.seed,
31
+ templateContext: extras.templateContext,
29
32
  },
30
33
  };
31
34
  }
@@ -37,10 +40,14 @@ describe("runner-template", () => {
37
40
  makeRuntimeConfig("frontend", { port: 3001, baseUrl: "http://127.0.0.1:{port}" }),
38
41
  ];
39
42
 
40
- expect([...buildPortMap(configs, "stack-2").entries()]).toEqual([
43
+ expect([...buildPortMap(configs, "runtime-2").entries()]).toEqual([
41
44
  ["api", 3100],
42
45
  ["frontend", 3101],
43
46
  ]);
47
+ expect([...buildPortMap(configs, "runtime-1", { index: 1, stride: 2 }).entries()]).toEqual([
48
+ ["api", 3200],
49
+ ["frontend", 3201],
50
+ ]);
44
51
 
45
52
  expect(() =>
46
53
  buildPortMap(
@@ -48,9 +55,10 @@ describe("runner-template", () => {
48
55
  makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
49
56
  makeRuntimeConfig("other", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
50
57
  ],
51
- "shared"
58
+ "runtime-1",
59
+ { index: 0, stride: 2 }
52
60
  )
53
- ).toThrow("Stack port collision");
61
+ ).toThrow("Runtime port collision");
54
62
  });
55
63
 
56
64
  it("resolves template strings and URL rewrites", () => {
@@ -59,12 +67,11 @@ describe("runner-template", () => {
59
67
  fs.mkdirSync(apiStateDir, { recursive: true });
60
68
  fs.writeFileSync(
61
69
  path.join(apiStateDir, "database_url"),
62
- "postgres://testkit:testkit@127.0.0.1:55432/onix_db"
70
+ "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
63
71
  );
64
72
 
65
73
  const context = {
66
- stackId: "stack-2",
67
- targetName: "frontend",
74
+ runtimeId: "runtime-2",
68
75
  serviceName: "frontend",
69
76
  serviceStateDir: "/tmp/state",
70
77
  portMap: new Map([
@@ -75,14 +82,16 @@ describe("runner-template", () => {
75
82
  readyUrlByService: new Map([["api", "http://127.0.0.1:3100/health"]]),
76
83
  stateDirByService: new Map([["api", apiStateDir]]),
77
84
  urlMappings: [["http://api:3000", "http://127.0.0.1:3100"]],
85
+ leaseId: "lease-1",
86
+ leaseDir: "/tmp/lease-1",
78
87
  };
79
88
 
80
- expect(resolveTemplateString("{stack}:{target}:{service}", context)).toBe(
81
- "stack-2:frontend:frontend"
89
+ expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
90
+ "runtime-2:frontend:lease-1"
82
91
  );
83
92
  expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
84
93
  expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
85
- "127.0.0.1:55432/onix_db"
94
+ "127.0.0.1:55432/runtime_db"
86
95
  );
87
96
  expect(finalizeString("API={baseUrl:api} OLD=http://api:3000", context)).toBe(
88
97
  "API=http://127.0.0.1:3100 OLD=http://127.0.0.1:3100"
@@ -92,8 +101,8 @@ describe("runner-template", () => {
92
101
  );
93
102
  });
94
103
 
95
- it("builds stack runtime configs and execution env", () => {
96
- const stackStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-stack-"));
104
+ it("builds runtime configs and execution env", () => {
105
+ const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-"));
97
106
  const api = makeRuntimeConfig(
98
107
  "api",
99
108
  {
@@ -120,22 +129,23 @@ describe("runner-template", () => {
120
129
  readyUrl: "http://127.0.0.1:{port}",
121
130
  env: {
122
131
  NEXT_PUBLIC_API_URL: "{baseUrl:api}",
123
- ONIX_DB_HOST: "{dbHost:api}",
132
+ API_DB_HOST: "{dbHost:api}",
124
133
  },
125
134
  });
126
135
 
127
- const resolved = resolveStackRuntimeConfigs(frontend, [api, frontend], "stack-2", stackStateDir);
136
+ const resolved = resolveRuntimeInstanceConfigs([api, frontend], "runtime-2", runtimeDir, {
137
+ graphDirName: "api__frontend",
138
+ portNamespaceIndex: 0,
139
+ portNamespaceStride: 2,
140
+ });
128
141
  expect(resolved[0].testkit.local.port).toBe(3100);
129
- expect(resolved[1].testkit.local.env.NEXT_PUBLIC_API_URL).toBe("{baseUrl:api}");
130
- expect(resolveServiceStateDir(stackStateDir, "frontend", api)).toBe(
131
- `${stackStateDir}/deps/api`
132
- );
133
- expect(getStackServiceStateDir(stackStateDir, "frontend", "frontend")).toBe(stackStateDir);
142
+ expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
143
+ expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);
134
144
 
135
- fs.mkdirSync(path.join(stackStateDir, "deps", "api"), { recursive: true });
145
+ fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
136
146
  fs.writeFileSync(
137
- path.join(stackStateDir, "deps", "api", "database_url"),
138
- "postgres://testkit:testkit@127.0.0.1:55432/onix_db"
147
+ path.join(runtimeDir, "services", "api", "database_url"),
148
+ "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
139
149
  );
140
150
 
141
151
  expect(
@@ -152,16 +162,38 @@ describe("runner-template", () => {
152
162
  ).toEqual({
153
163
  PATH: "/usr/bin",
154
164
  NEXT_PUBLIC_API_URL: "http://127.0.0.1:3100",
155
- ONIX_DB_HOST: "127.0.0.1",
165
+ API_DB_HOST: "127.0.0.1",
156
166
  TESTKIT_ACTIVE: "1",
157
- TESTKIT_STACK_ID: "stack-2",
167
+ TESTKIT_RUNTIME_ID: "runtime-2",
168
+ });
169
+
170
+ expect(
171
+ buildTaskExecutionEnv(
172
+ resolved[1],
173
+ {
174
+ leaseId: "lease-1",
175
+ leaseDir: "/tmp/lease-1",
176
+ },
177
+ {},
178
+ {}
179
+ )
180
+ ).toMatchObject({
181
+ TESTKIT_RUNTIME_ID: "runtime-2",
182
+ TESTKIT_LEASE_ID: "lease-1",
183
+ TESTKIT_LEASE_DIR: "/tmp/lease-1",
158
184
  });
159
185
 
160
186
  expect(
161
- buildPlaywrightEnv({ stackId: "shared", testkit: { serviceEnv: {} } }, "http://localhost:3000", {})
187
+ buildPlaywrightEnv(
188
+ { runtimeId: "runtime-1", testkit: { serviceEnv: {}, templateContext: {} } },
189
+ "http://localhost:3000",
190
+ { leaseId: "lease-2", leaseDir: "/tmp/lease-2" },
191
+ {}
192
+ )
162
193
  ).toMatchObject({
163
194
  BASE_URL: "http://localhost:3000",
164
- TESTKIT_STACK_ID: "shared",
195
+ TESTKIT_RUNTIME_ID: "runtime-1",
196
+ TESTKIT_LEASE_ID: "lease-2",
165
197
  PLAYWRIGHT_HTML_OPEN: "never",
166
198
  });
167
199
  });
@@ -1,6 +1,6 @@
1
1
  import { formatError } from "./formatting.mjs";
2
- import { runDalBatch, runHttpK6Batch } from "./default-runtime-runner.mjs";
3
- import { runPlaywrightBatch } from "./playwright-runner.mjs";
2
+ import { runDalTask, runHttpK6Task } from "./default-runtime-runner.mjs";
3
+ import { runPlaywrightTask } from "./playwright-runner.mjs";
4
4
 
5
5
  const HTTP_K6_TYPES = new Set(["integration", "e2e", "load"]);
6
6
 
@@ -17,11 +17,11 @@ export function createWorker(workerId, productDir) {
17
17
  export async function runWorker(
18
18
  worker,
19
19
  queue,
20
- stackManager,
20
+ runtimeManager,
21
21
  trackers,
22
22
  timingUpdates,
23
23
  lifecycle,
24
- claimNextBatch,
24
+ claimNextTask,
25
25
  recordTaskOutcome,
26
26
  recordGraphError
27
27
  ) {
@@ -32,32 +32,29 @@ export async function runWorker(
32
32
  try {
33
33
  while (true) {
34
34
  if (lifecycle.isStopRequested()) break;
35
- const batch = claimNextBatch(queue, worker.currentGraphKey);
36
- if (!batch) break;
35
+ const task = claimNextTask(queue, worker.currentGraphKey);
36
+ if (!task) break;
37
37
 
38
38
  let lease = null;
39
39
  try {
40
- if (worker.currentGraphKey && worker.currentGraphKey !== batch.graphKey) {
40
+ if (worker.currentGraphKey && worker.currentGraphKey !== task.graphKey) {
41
41
  worker.graphSwitches += 1;
42
42
  }
43
- worker.currentGraphKey = batch.graphKey;
44
- lease = await stackManager.acquire(batch);
45
- const outcomes = await runBatch(lease.context, batch, lifecycle);
46
- for (const outcome of outcomes) {
47
- recordTaskOutcome(trackers, outcome.task, outcome);
48
- timingUpdates.push({
49
- key: outcome.task.timingKey,
50
- durationMs: outcome.durationMs,
51
- });
52
- worker.taskCount += 1;
53
- }
54
- await stackManager.release(lease, { accessMode: batch.accessMode });
43
+ worker.currentGraphKey = task.graphKey;
44
+ lease = await runtimeManager.acquire(task);
45
+ const outcome = await runTask(lease.context, task, lifecycle, lease);
46
+ recordTaskOutcome(trackers, outcome.task, outcome);
47
+ timingUpdates.push({
48
+ key: outcome.task.timingKey,
49
+ durationMs: outcome.durationMs,
50
+ });
51
+ worker.taskCount += 1;
52
+ await runtimeManager.release(lease);
55
53
  } catch (error) {
56
54
  const message = formatError(error);
57
55
  errors.push(message);
58
- recordGraphError(trackers, { assignedTargets: lease?.context?.assignedTargets || [batch.targetName] }, message);
59
- await stackManager.release(lease, {
60
- accessMode: batch.accessMode,
56
+ recordGraphError(trackers, { targetNames: lease?.context?.targetNames || [task.targetName] }, message);
57
+ await runtimeManager.release(lease, {
61
58
  invalidate: lease !== null,
62
59
  });
63
60
  }
@@ -76,23 +73,23 @@ export async function runWorker(
76
73
  };
77
74
  }
78
75
 
79
- async function runBatch(context, batch, lifecycle) {
80
- const targetConfig = context.configByName.get(batch.targetName);
76
+ async function runTask(context, task, lifecycle, lease) {
77
+ const targetConfig = context.configByName.get(task.targetName);
81
78
  if (!targetConfig) {
82
- throw new Error(`Stack is missing target config "${batch.targetName}"`);
79
+ throw new Error(`Runtime instance is missing target config "${task.targetName}"`);
83
80
  }
84
81
 
85
- if (batch.framework === "playwright") {
86
- return runPlaywrightBatch(targetConfig, batch, lifecycle);
82
+ if (task.framework === "playwright") {
83
+ return runPlaywrightTask(targetConfig, task, lifecycle, lease);
87
84
  }
88
- if (batch.type === "dal") {
89
- return runDalBatch(targetConfig, batch, lifecycle);
85
+ if (task.type === "dal") {
86
+ return runDalTask(targetConfig, task, lifecycle, lease);
90
87
  }
91
- if (batch.framework === "k6" && HTTP_K6_TYPES.has(batch.type)) {
92
- return runHttpK6Batch(targetConfig, batch, lifecycle);
88
+ if (task.framework === "k6" && HTTP_K6_TYPES.has(task.type)) {
89
+ return runHttpK6Task(targetConfig, task, lifecycle, lease);
93
90
  }
94
91
 
95
92
  throw new Error(
96
- `Unsupported task combination for ${batch.targetName}: type=${batch.type} framework=${batch.framework}`
93
+ `Unsupported task combination for ${task.targetName}: type=${task.type} framework=${task.framework}`
97
94
  );
98
95
  }