@elench/testkit 0.1.48 → 0.1.50

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.
@@ -113,12 +113,14 @@ export function resolveRuntimeConfig(
113
113
  urlMappings
114
114
  ) {
115
115
  const stateDir = resolveServiceStateDir(runtimeDir, config);
116
+ const prepareDir = resolveServicePrepareDir(runtimeDir, config);
116
117
  const context = {
117
118
  runtimeId,
118
119
  runtimeLabel,
119
120
  runtimeDir,
120
121
  serviceName: config.name,
121
122
  serviceStateDir: stateDir,
123
+ prepareDir,
122
124
  portMap,
123
125
  baseUrlByService,
124
126
  readyUrlByService,
@@ -135,6 +137,11 @@ export function resolveRuntimeConfig(
135
137
  }
136
138
  : undefined;
137
139
 
140
+ const runtime = {
141
+ ...config.testkit.runtime,
142
+ prepare: finalizeRuntimePrepare(config.testkit.runtime.prepare, context),
143
+ };
144
+
138
145
  const local = config.testkit.local
139
146
  ? {
140
147
  ...config.testkit.local,
@@ -158,7 +165,9 @@ export function resolveRuntimeConfig(
158
165
  testkit: {
159
166
  ...config.testkit,
160
167
  database,
168
+ prepareDir,
161
169
  templateContext: context,
170
+ runtime,
162
171
  local,
163
172
  },
164
173
  };
@@ -197,6 +206,10 @@ export function resolveServiceStateDir(runtimeDir, config) {
197
206
  return path.join(runtimeDir, "services", config.name);
198
207
  }
199
208
 
209
+ export function resolveServicePrepareDir(runtimeDir, config) {
210
+ return path.join(resolveServiceStateDir(runtimeDir, config), "prepared");
211
+ }
212
+
200
213
  export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
201
214
  return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
202
215
  }
@@ -244,6 +257,7 @@ function buildTemplateContext(config, lease) {
244
257
  runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
245
258
  serviceName: config.name,
246
259
  serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
260
+ prepareDir: config.testkit?.prepareDir || baseContext.prepareDir || null,
247
261
  leaseId: lease?.leaseId || null,
248
262
  leaseDir: lease?.leaseDir || null,
249
263
  };
@@ -280,6 +294,8 @@ export function resolveTemplateString(value, context) {
280
294
  return context.serviceName;
281
295
  case "stateDir":
282
296
  return context.serviceStateDir;
297
+ case "prepareDir":
298
+ return context.prepareDir || "";
283
299
  case "lease":
284
300
  return context.leaseId ? String(context.leaseId) : "";
285
301
  case "leaseDir":
@@ -332,6 +348,31 @@ function resolveEnvTemplates(values, templateContext) {
332
348
  );
333
349
  }
334
350
 
351
+ function finalizeRuntimePrepare(prepare, context) {
352
+ if (!prepare) {
353
+ return {
354
+ inputs: [],
355
+ steps: [],
356
+ };
357
+ }
358
+
359
+ const finalizeStep = (step) => ({
360
+ ...step,
361
+ ...(typeof step.cmd === "string" ? { cmd: finalizeString(step.cmd, context) } : {}),
362
+ ...(typeof step.cwd === "string" ? { cwd: finalizeString(step.cwd, context) } : {}),
363
+ ...(typeof step.path === "string" ? { path: finalizeString(step.path, context) } : {}),
364
+ ...(typeof step.specifier === "string"
365
+ ? { specifier: finalizeString(step.specifier, context) }
366
+ : {}),
367
+ inputs: (step.inputs || []).map((input) => finalizeString(input, context)),
368
+ });
369
+
370
+ return {
371
+ inputs: (prepare.inputs || []).map((input) => finalizeString(input, context)),
372
+ steps: (prepare.steps || []).map(finalizeStep),
373
+ };
374
+ }
375
+
335
376
  function resolveDatabaseTemplateValue(token, serviceName, context) {
336
377
  const stateDir = context.stateDirByService?.get(serviceName);
337
378
  if (!stateDir) {
@@ -22,8 +22,18 @@ function makeRuntimeConfig(name, local, extras = {}) {
22
22
  name,
23
23
  stateDir: extras.stateDir,
24
24
  runtimeId: extras.runtimeId,
25
+ productDir: extras.productDir || process.cwd(),
25
26
  testkit: {
26
27
  local,
28
+ runtime: extras.runtime || {
29
+ instances: 1,
30
+ maxConcurrentTasks: Infinity,
31
+ prepare: {
32
+ inputs: [],
33
+ steps: [],
34
+ },
35
+ },
36
+ envFiles: extras.envFiles || [],
27
37
  serviceEnv: extras.serviceEnv || {},
28
38
  databaseFrom: extras.databaseFrom,
29
39
  database: extras.database,
@@ -88,6 +98,9 @@ describe("runner-template", () => {
88
98
  expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
89
99
  "runtime-2:frontend:lease-1"
90
100
  );
101
+ expect(resolveTemplateString("{prepareDir}", { ...context, prepareDir: "/tmp/prepare-1" })).toBe(
102
+ "/tmp/prepare-1"
103
+ );
91
104
  expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
92
105
  expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
93
106
  "127.0.0.1:55432/runtime_db"
@@ -140,6 +153,7 @@ describe("runner-template", () => {
140
153
  expect(resolved[0].testkit.local.port).toBe(3100);
141
154
  expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
142
155
  expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);
156
+ expect(resolved[0].testkit.prepareDir).toBe(`${runtimeDir}/services/api/prepared`);
143
157
 
144
158
  fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
145
159
  fs.writeFileSync(
@@ -197,6 +211,56 @@ describe("runner-template", () => {
197
211
  });
198
212
  });
199
213
 
214
+ it("finalizes runtime.prepare templates with prepareDir", () => {
215
+ const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-prepare-"));
216
+ const api = makeRuntimeConfig(
217
+ "api",
218
+ {
219
+ cwd: ".",
220
+ start: "npm run api",
221
+ port: 3000,
222
+ baseUrl: "http://127.0.0.1:{port}",
223
+ readyUrl: "http://127.0.0.1:{port}/health",
224
+ env: {},
225
+ },
226
+ {
227
+ runtime: {
228
+ instances: 1,
229
+ maxConcurrentTasks: 2,
230
+ prepare: {
231
+ inputs: ["src/{service}.ts"],
232
+ steps: [
233
+ {
234
+ kind: "command",
235
+ cmd: "node scripts/prepare.mjs {prepareDir}",
236
+ cwd: ".",
237
+ inputs: ["src/{service}.ts"],
238
+ },
239
+ ],
240
+ },
241
+ },
242
+ }
243
+ );
244
+
245
+ const [resolved] = resolveRuntimeInstanceConfigs([api], "runtime-1", runtimeDir, {
246
+ graphDirName: "api",
247
+ portNamespaceIndex: 0,
248
+ portNamespaceStride: 1,
249
+ });
250
+
251
+ expect(resolved.testkit.runtime.prepare).toEqual({
252
+ inputs: ["src/api.ts"],
253
+ steps: [
254
+ {
255
+ kind: "command",
256
+ cmd: `node scripts/prepare.mjs ${runtimeDir}/services/api/prepared`,
257
+ cwd: ".",
258
+ inputs: ["src/api.ts"],
259
+ },
260
+ ],
261
+ });
262
+ });
263
+
200
264
  it("parses runtime sockets", () => {
201
265
  expect(numericPortFromUrl("http://localhost:3000")).toBe(3000);
202
266
  expect(socketFromUrl("http://localhost:3000")).toEqual({
@@ -36,6 +36,27 @@ export async function runWorker(
36
36
  runtimeManager.canAcquire(candidate)
37
37
  );
38
38
  if (!task) {
39
+ const blockedTasks = runtimeManager.drainFailedTasks(queue);
40
+ if (blockedTasks.length > 0) {
41
+ const now = Date.now();
42
+ const recordedGraphs = new Set();
43
+ for (const blocked of blockedTasks) {
44
+ recordTaskOutcome(trackers, blocked.task, {
45
+ failed: false,
46
+ status: "not_run",
47
+ reason: blocked.reason,
48
+ durationMs: 0,
49
+ startedAt: now,
50
+ finishedAt: now,
51
+ error: null,
52
+ });
53
+ if (!recordedGraphs.has(blocked.graph.key)) {
54
+ recordGraphError(trackers, blocked.graph, blocked.message, now);
55
+ recordedGraphs.add(blocked.graph.key);
56
+ }
57
+ }
58
+ continue;
59
+ }
39
60
  if (queue.length === 0) break;
40
61
  await new Promise((resolve) => setTimeout(resolve, 10));
41
62
  continue;
@@ -58,6 +58,10 @@ export interface SkipConfig {
58
58
  export interface RuntimeConfig {
59
59
  instances?: number;
60
60
  maxConcurrentTasks?: number;
61
+ prepare?: {
62
+ inputs?: string[];
63
+ steps?: TemplateLifecycleStepConfig[];
64
+ };
61
65
  }
62
66
 
63
67
  export interface SuiteRequirementRule {
@@ -85,7 +85,7 @@ export function nextService(options = {}) {
85
85
  ...service(options),
86
86
  local: {
87
87
  cwd,
88
- start: options.start || "exec ./node_modules/.bin/next dev -p {port}",
88
+ start: options.start || "./node_modules/.bin/next dev -p {port}",
89
89
  port,
90
90
  baseUrl,
91
91
  readyUrl: options.readyUrl || baseUrl,
@@ -105,7 +105,7 @@ export function tsxService(options = {}) {
105
105
  ...service(options),
106
106
  local: {
107
107
  cwd,
108
- start: options.start || `exec ./node_modules/.bin/tsx watch ${entry}`,
108
+ start: options.start || `./node_modules/.bin/tsx watch ${entry}`,
109
109
  port,
110
110
  baseUrl,
111
111
  readyUrl: options.readyUrl || `${baseUrl}${options.readyPath || "/health"}`,
@@ -196,12 +196,12 @@ export {
196
196
 
197
197
  function defaultGoStartCommand(options) {
198
198
  if (options.command) {
199
- return `exec go run ${options.command}`;
199
+ return `go run ${options.command}`;
200
200
  }
201
201
  if (options.entrypoint) {
202
- return `exec go run ${options.entrypoint}`;
202
+ return `go run ${options.entrypoint}`;
203
203
  }
204
- return "exec go run ./cmd/server";
204
+ return "go run ./cmd/server";
205
205
  }
206
206
 
207
207
  function requiredNumber(value, label) {
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { goService, nextService, tsxService } from "./index.mjs";
3
+
4
+ describe("setup helpers", () => {
5
+ it("emits plain next start commands without an exec prefix", () => {
6
+ const config = nextService({ port: 3000 });
7
+
8
+ expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
9
+ });
10
+
11
+ it("emits plain tsx start commands without an exec prefix", () => {
12
+ const config = tsxService({ port: 3000, entry: "src/server.ts" });
13
+
14
+ expect(config.local.start).toBe("./node_modules/.bin/tsx watch src/server.ts");
15
+ });
16
+
17
+ it("emits plain go start commands without an exec prefix", () => {
18
+ expect(goService({ port: 3000 }).local.start).toBe("go run ./cmd/server");
19
+ expect(goService({ port: 3000, entrypoint: "./cmd/api" }).local.start).toBe(
20
+ "go run ./cmd/api"
21
+ );
22
+ expect(goService({ port: 3000, command: "./cmd/worker" }).local.start).toBe(
23
+ "go run ./cmd/worker"
24
+ );
25
+ });
26
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",