@elench/testkit 0.1.51 → 0.1.53

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 (40) hide show
  1. package/README.md +42 -7
  2. package/bin/testkit.mjs +4 -6
  3. package/lib/cli/command-helpers.mjs +170 -0
  4. package/lib/cli/commands/artifacts.mjs +45 -0
  5. package/lib/cli/commands/cleanup.mjs +15 -0
  6. package/lib/cli/commands/db/snapshot/capture.mjs +22 -0
  7. package/lib/cli/commands/destroy.mjs +15 -0
  8. package/lib/cli/commands/known-failures/render.mjs +19 -0
  9. package/lib/cli/commands/known-failures/validate.mjs +20 -0
  10. package/lib/cli/commands/logs.mjs +47 -0
  11. package/lib/cli/commands/run.mjs +23 -0
  12. package/lib/cli/commands/show.mjs +47 -0
  13. package/lib/cli/commands/status.mjs +15 -0
  14. package/lib/cli/commands/watch.mjs +23 -0
  15. package/lib/cli/entrypoint.mjs +83 -0
  16. package/lib/cli/index.mjs +6 -116
  17. package/lib/cli/presentation/run-reporter.mjs +91 -0
  18. package/lib/cli/tui/watch-app.mjs +104 -0
  19. package/lib/cli/viewer.mjs +163 -0
  20. package/lib/runner/artifacts.mjs +35 -0
  21. package/lib/runner/default-runtime-runner.mjs +44 -10
  22. package/lib/runner/formatting.mjs +97 -0
  23. package/lib/runner/formatting.test.mjs +4 -6
  24. package/lib/runner/logs.mjs +72 -0
  25. package/lib/runner/orchestrator.mjs +41 -19
  26. package/lib/runner/playwright-runner.mjs +15 -7
  27. package/lib/runner/processes.mjs +9 -11
  28. package/lib/runner/reporting.mjs +5 -1
  29. package/lib/runner/reporting.test.mjs +4 -1
  30. package/lib/runner/runtime-contexts.mjs +7 -3
  31. package/lib/runner/runtime-manager.mjs +8 -2
  32. package/lib/runner/runtime-preparation.mjs +9 -4
  33. package/lib/runner/services.mjs +25 -8
  34. package/lib/runner/template-steps.mjs +4 -3
  35. package/lib/runner/worker-loop.mjs +8 -7
  36. package/lib/setup/index.d.ts +46 -13
  37. package/lib/setup/index.mjs +47 -0
  38. package/lib/setup/index.test.mjs +109 -1
  39. package/lib/toolchains/index.mjs +6 -3
  40. package/package.json +11 -3
@@ -6,16 +6,16 @@ import {
6
6
  } from "../toolchains/index.mjs";
7
7
  import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
8
8
  import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
9
- import { killChildProcess, pipeOutput, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
9
+ import { captureOutput, killChildProcess, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
10
10
  import { readDatabaseUrl } from "./state-io.mjs";
11
11
 
12
- export async function startLocalServices(runtimeConfigs, lifecycle) {
12
+ export async function startLocalServices(runtimeConfigs, lifecycle, options = {}) {
13
13
  const started = [];
14
14
 
15
15
  try {
16
16
  for (const config of runtimeConfigs) {
17
17
  if (!config.testkit.local) continue;
18
- const proc = await startLocalService(config, lifecycle);
18
+ const proc = await startLocalService(config, lifecycle, options);
19
19
  started.push(proc);
20
20
  }
21
21
  } catch (error) {
@@ -26,10 +26,10 @@ export async function startLocalServices(runtimeConfigs, lifecycle) {
26
26
  return started;
27
27
  }
28
28
 
29
- export async function startLocalService(config, lifecycle) {
29
+ export async function startLocalService(config, lifecycle, options = {}) {
30
30
  const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
31
31
  const resolvedToolchain = await resolveConfiguredToolchain(config);
32
- await announceResolvedToolchain(config, resolvedToolchain);
32
+ await announceResolvedToolchain(config, resolvedToolchain, options.reporter);
33
33
  const env = applyToolchainEnv(
34
34
  buildExecutionEnv(config, config.testkit.local.env, process.env),
35
35
  resolvedToolchain
@@ -46,12 +46,29 @@ export async function startLocalService(config, lifecycle) {
46
46
 
47
47
  await assertLocalServicePortsAvailable(config, isPortInUse);
48
48
 
49
- console.log(`Starting ${config.runtimeLabel}:${config.name}: ${config.testkit.local.start}`);
49
+ options.reporter?.localServiceStarting?.(config, config.testkit.local.start);
50
50
  const child = startDetachedCommand(config.testkit.local.start, cwd, env);
51
+ const logRecord = options.logRegistry?.ensureServiceLogRecord(config);
52
+ const liveWriter =
53
+ options.reporter?.outputMode === "debug"
54
+ ? (line) => options.reporter.writeDebugLine?.(line)
55
+ : null;
51
56
 
52
57
  const outputDrains = [
53
- pipeOutput(child.stdout, `[${config.runtimeLabel}:${config.name}]`),
54
- pipeOutput(child.stderr, `[${config.runtimeLabel}:${config.name}]`),
58
+ captureOutput(child.stdout, {
59
+ livePrefix: `[${config.runtimeLabel}:${config.name}]`,
60
+ liveWriter,
61
+ onLine(line) {
62
+ if (logRecord) options.logRegistry.append(logRecord, "stdout", line);
63
+ },
64
+ }),
65
+ captureOutput(child.stderr, {
66
+ livePrefix: `[${config.runtimeLabel}:${config.name}]`,
67
+ liveWriter,
68
+ onLine(line) {
69
+ if (logRecord) options.logRegistry.append(logRecord, "stderr", line);
70
+ },
71
+ }),
55
72
  ];
56
73
  lifecycle.registerService(config, child, cwd, () => {
57
74
  killChildProcess(child, "SIGTERM");
@@ -23,14 +23,15 @@ const MODULE_RUNNER_ENTRY = path.join(
23
23
  "template-step-module-runner.mjs"
24
24
  );
25
25
 
26
- export async function runConfiguredSteps({ config, steps = [], env, labelPrefix }) {
26
+ export async function runConfiguredSteps({ config, steps = [], env, labelPrefix, reporter = null }) {
27
27
  if (steps.length === 0) return;
28
28
  const resolvedToolchain = await resolveConfiguredToolchain(config);
29
- await announceResolvedToolchain(config, resolvedToolchain);
29
+ await announceResolvedToolchain(config, resolvedToolchain, reporter);
30
30
 
31
31
  for (const [index, step] of steps.entries()) {
32
32
  const label = `${labelPrefix}:${config.name}:${index + 1}`;
33
- console.log(`\n── ${label} ──`);
33
+ if (reporter?.phaseStarted) reporter.phaseStarted(label);
34
+ else console.log(`\n── ${label} ──`);
34
35
  await runConfiguredStep(config, step, env, resolvedToolchain);
35
36
  }
36
37
  }
@@ -23,10 +23,10 @@ export async function runWorker(
23
23
  lifecycle,
24
24
  claimNextTask,
25
25
  recordTaskOutcome,
26
- recordGraphError
26
+ recordGraphError,
27
+ reporter = null
27
28
  ) {
28
29
  const startedAt = Date.now();
29
- console.log(`\n══ worker ${worker.workerId} ══`);
30
30
  const errors = [];
31
31
 
32
32
  try {
@@ -69,8 +69,9 @@ export async function runWorker(
69
69
  }
70
70
  worker.currentGraphKey = task.graphKey;
71
71
  lease = await runtimeManager.acquire(task);
72
- const outcome = await runTask(lease.context, task, lifecycle, lease);
72
+ const outcome = await runTask(lease.context, task, lifecycle, lease, reporter);
73
73
  recordTaskOutcome(trackers, outcome.task, outcome);
74
+ reporter?.taskFinished?.(outcome.task, outcome);
74
75
  timingUpdates.push({
75
76
  key: outcome.task.timingKey,
76
77
  durationMs: outcome.durationMs,
@@ -100,20 +101,20 @@ export async function runWorker(
100
101
  };
101
102
  }
102
103
 
103
- async function runTask(context, task, lifecycle, lease) {
104
+ async function runTask(context, task, lifecycle, lease, reporter = null) {
104
105
  const targetConfig = context.configByName.get(task.targetName);
105
106
  if (!targetConfig) {
106
107
  throw new Error(`Runtime instance is missing target config "${task.targetName}"`);
107
108
  }
108
109
 
109
110
  if (task.framework === "playwright") {
110
- return runPlaywrightTask(targetConfig, task, lifecycle, lease);
111
+ return runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter);
111
112
  }
112
113
  if (task.type === "dal") {
113
- return runDalTask(targetConfig, task, lifecycle, lease);
114
+ return runDalTask(targetConfig, task, lifecycle, lease, reporter);
114
115
  }
115
116
  if (task.framework === "k6" && HTTP_K6_TYPES.has(task.type)) {
116
- return runHttpK6Task(targetConfig, task, lifecycle, lease);
117
+ return runHttpK6Task(targetConfig, task, lifecycle, lease, reporter);
117
118
  }
118
119
 
119
120
  throw new Error(
@@ -1,18 +1,18 @@
1
1
  import type { AuthAdapter, HeaderBuilder, HttpSuiteConfig } from "../index";
2
2
 
3
- export interface LocalDatabaseConfig {
4
- provider: "local";
5
- binding?: "shared" | "per-runtime";
6
- image?: string;
7
- password?: string;
8
- reset?: boolean;
9
- template?: {
10
- inputs?: string[];
11
- migrate?: TemplateLifecycleStepConfig[];
12
- seed?: TemplateLifecycleStepConfig[];
13
- verify?: TemplateLifecycleStepConfig[];
14
- };
15
- user?: string;
3
+ export interface DatabaseTemplateConfig {
4
+ inputs?: string[];
5
+ migrate?: TemplateLifecycleStepConfig[];
6
+ seed?: TemplateLifecycleStepConfig[];
7
+ verify?: TemplateLifecycleStepConfig[];
8
+ }
9
+
10
+ export interface SeededDatabaseTemplateOptions {
11
+ inputs?: string[];
12
+ schema?: string | TemplateSqlFileStepConfig;
13
+ migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
14
+ seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
15
+ verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
16
16
  }
17
17
 
18
18
  export interface TemplateStepBaseConfig {
@@ -40,6 +40,16 @@ export type TemplateLifecycleStepConfig =
40
40
  | TemplateSqlFileStepConfig
41
41
  | TemplateModuleStepConfig;
42
42
 
43
+ export interface LocalDatabaseConfig {
44
+ provider: "local";
45
+ binding?: "shared" | "per-runtime";
46
+ image?: string;
47
+ password?: string;
48
+ reset?: boolean;
49
+ template?: DatabaseTemplateConfig;
50
+ user?: string;
51
+ }
52
+
43
53
  export interface SkipFileRule {
44
54
  path: string;
45
55
  reason: string;
@@ -161,6 +171,29 @@ export declare function moduleStep(
161
171
  specifier: string,
162
172
  options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
163
173
  ): TemplateModuleStepConfig;
174
+ export declare function schemaSql(
175
+ filePath: string,
176
+ options?: Omit<TemplateSqlFileStepConfig, "kind" | "path">
177
+ ): TemplateSqlFileStepConfig;
178
+ export declare function seedCommand(
179
+ cmd: string,
180
+ options?: Omit<TemplateCommandStepConfig, "kind" | "cmd">
181
+ ): TemplateCommandStepConfig;
182
+ export declare function seedModule(
183
+ specifier: string,
184
+ options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
185
+ ): TemplateModuleStepConfig;
186
+ export declare function verifyCommand(
187
+ cmd: string,
188
+ options?: Omit<TemplateCommandStepConfig, "kind" | "cmd">
189
+ ): TemplateCommandStepConfig;
190
+ export declare function verifyModule(
191
+ specifier: string,
192
+ options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
193
+ ): TemplateModuleStepConfig;
194
+ export declare function seededDatabaseTemplate(
195
+ options?: SeededDatabaseTemplateOptions
196
+ ): DatabaseTemplateConfig;
164
197
  export declare function nodeToolchain(options?: NodeToolchainConfig): NodeToolchainConfig;
165
198
  export declare function goService(options: ServiceConfig["local"] & {
166
199
  command?: string;
@@ -56,6 +56,40 @@ export function moduleStep(specifier, options = {}) {
56
56
  };
57
57
  }
58
58
 
59
+ export function schemaSql(filePath, options = {}) {
60
+ return sqlFileStep(filePath, options);
61
+ }
62
+
63
+ export function seedCommand(cmd, options = {}) {
64
+ return commandStep(cmd, options);
65
+ }
66
+
67
+ export function seedModule(specifier, options = {}) {
68
+ return moduleStep(specifier, options);
69
+ }
70
+
71
+ export function verifyCommand(cmd, options = {}) {
72
+ return commandStep(cmd, options);
73
+ }
74
+
75
+ export function verifyModule(specifier, options = {}) {
76
+ return moduleStep(specifier, options);
77
+ }
78
+
79
+ export function seededDatabaseTemplate(options = {}) {
80
+ const migrate = normalizeTemplateStepList(options.migrate);
81
+ const seed = normalizeTemplateStepList(options.seed);
82
+ const verify = normalizeTemplateStepList(options.verify);
83
+ const schema = normalizeSchemaStep(options.schema);
84
+
85
+ return {
86
+ inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
87
+ migrate: schema ? [schema, ...migrate] : migrate,
88
+ seed,
89
+ verify,
90
+ };
91
+ }
92
+
59
93
  export function nodeToolchain(options = {}) {
60
94
  return {
61
95
  kind: "node",
@@ -218,6 +252,19 @@ function requiredNumber(value, label) {
218
252
  return value;
219
253
  }
220
254
 
255
+ function normalizeTemplateStepList(value) {
256
+ if (value == null) return [];
257
+ return Array.isArray(value) ? [...value] : [value];
258
+ }
259
+
260
+ function normalizeSchemaStep(value) {
261
+ if (value == null) return null;
262
+ if (typeof value === "string") {
263
+ return sqlFileStep(value);
264
+ }
265
+ return value;
266
+ }
267
+
221
268
  function envValue(name) {
222
269
  const env = getRuntimeEnv();
223
270
  const value = env?.rawEnv?.[name] || env?.[name];
@@ -1,5 +1,16 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { goService, nextService, nodeToolchain, tsxService } from "./index.mjs";
2
+ import {
3
+ goService,
4
+ nextService,
5
+ nodeToolchain,
6
+ schemaSql,
7
+ seedCommand,
8
+ seedModule,
9
+ seededDatabaseTemplate,
10
+ tsxService,
11
+ verifyCommand,
12
+ verifyModule,
13
+ } from "./index.mjs";
3
14
 
4
15
  describe("setup helpers", () => {
5
16
  it("emits plain next start commands without an exec prefix", () => {
@@ -31,4 +42,101 @@ describe("setup helpers", () => {
31
42
  install: "download",
32
43
  });
33
44
  });
45
+
46
+ it("emits semantic database template steps using the underlying step shapes", () => {
47
+ expect(schemaSql("db/schema.sql")).toEqual({
48
+ kind: "sql-file",
49
+ path: "db/schema.sql",
50
+ cwd: undefined,
51
+ inputs: undefined,
52
+ });
53
+ expect(seedCommand("npm run db:seed")).toEqual({
54
+ kind: "command",
55
+ cmd: "npm run db:seed",
56
+ cwd: undefined,
57
+ inputs: undefined,
58
+ });
59
+ expect(seedModule("scripts/seed.ts#seed")).toEqual({
60
+ kind: "module",
61
+ specifier: "scripts/seed.ts#seed",
62
+ cwd: undefined,
63
+ inputs: undefined,
64
+ });
65
+ expect(verifyCommand("npm run db:verify")).toEqual({
66
+ kind: "command",
67
+ cmd: "npm run db:verify",
68
+ cwd: undefined,
69
+ inputs: undefined,
70
+ });
71
+ expect(verifyModule("scripts/verify.ts#verify")).toEqual({
72
+ kind: "module",
73
+ specifier: "scripts/verify.ts#verify",
74
+ cwd: undefined,
75
+ inputs: undefined,
76
+ });
77
+ });
78
+
79
+ it("builds a seeded database template from schema, seed, and verify intents", () => {
80
+ expect(
81
+ seededDatabaseTemplate({
82
+ inputs: ["db/schema.sql", "scripts/seed.ts"],
83
+ schema: "db/schema.sql",
84
+ seed: seedCommand("npm run db:seed"),
85
+ verify: verifyModule("scripts/verify.ts#verifySeed"),
86
+ })
87
+ ).toEqual({
88
+ inputs: ["db/schema.sql", "scripts/seed.ts"],
89
+ migrate: [
90
+ {
91
+ kind: "sql-file",
92
+ path: "db/schema.sql",
93
+ cwd: undefined,
94
+ inputs: undefined,
95
+ },
96
+ ],
97
+ seed: [
98
+ {
99
+ kind: "command",
100
+ cmd: "npm run db:seed",
101
+ cwd: undefined,
102
+ inputs: undefined,
103
+ },
104
+ ],
105
+ verify: [
106
+ {
107
+ kind: "module",
108
+ specifier: "scripts/verify.ts#verifySeed",
109
+ cwd: undefined,
110
+ inputs: undefined,
111
+ },
112
+ ],
113
+ });
114
+ });
115
+
116
+ it("prepends schema before explicit migrate steps and normalizes singletons to arrays", () => {
117
+ expect(
118
+ seededDatabaseTemplate({
119
+ schema: schemaSql("db/schema.sql", { cwd: "db" }),
120
+ migrate: seedCommand("echo migrate"),
121
+ })
122
+ ).toEqual({
123
+ inputs: undefined,
124
+ migrate: [
125
+ {
126
+ kind: "sql-file",
127
+ path: "db/schema.sql",
128
+ cwd: "db",
129
+ inputs: undefined,
130
+ },
131
+ {
132
+ kind: "command",
133
+ cmd: "echo migrate",
134
+ cwd: undefined,
135
+ inputs: undefined,
136
+ },
137
+ ],
138
+ seed: [],
139
+ verify: [],
140
+ });
141
+ });
34
142
  });
@@ -84,12 +84,15 @@ export async function resolveConfiguredToolchain(config, options = {}) {
84
84
  }
85
85
  }
86
86
 
87
- export async function announceResolvedToolchain(config, resolvedToolchain) {
87
+ export async function announceResolvedToolchain(config, resolvedToolchain, reporter = null) {
88
88
  if (!resolvedToolchain || announcedToolchains.has(config)) return;
89
89
  announcedToolchains.add(config);
90
+ if (reporter?.toolchainResolved) {
91
+ reporter.toolchainResolved(config, resolvedToolchain);
92
+ return;
93
+ }
90
94
  console.log(
91
- `[testkit] ${config.runtimeLabel || config.name}:${config.name} toolchain ` +
92
- `${resolvedToolchain.summary}`
95
+ `[testkit] ${config.runtimeLabel || config.name}:${config.name} toolchain ${resolvedToolchain.summary}`
93
96
  );
94
97
  }
95
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
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",
@@ -26,6 +26,12 @@
26
26
  "bin": {
27
27
  "testkit": "bin/testkit.mjs"
28
28
  },
29
+ "oclif": {
30
+ "bin": "testkit",
31
+ "commands": "./lib/cli/commands",
32
+ "default": "run",
33
+ "topicSeparator": " "
34
+ },
29
35
  "scripts": {
30
36
  "test": "vitest run",
31
37
  "test:unit": "vitest run lib",
@@ -42,9 +48,11 @@
42
48
  "vitest": "^3.2.4"
43
49
  },
44
50
  "dependencies": {
45
- "cac": "^6.7.14",
51
+ "@oclif/core": "^4.10.6",
46
52
  "esbuild": "^0.25.11",
47
- "execa": "^9.5.0"
53
+ "execa": "^9.5.0",
54
+ "ink": "^7.0.1",
55
+ "react": "^19.2.5"
48
56
  },
49
57
  "engines": {
50
58
  "node": ">=18"