@elench/testkit 0.1.96 → 0.1.98
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.
- package/lib/app/browser-bridge.mjs +1 -1
- package/lib/cli/assistant/app.mjs +49 -12
- package/lib/cli/assistant/composer.mjs +19 -1
- package/lib/cli/assistant/context-pack.mjs +9 -8
- package/lib/cli/assistant/interactive.mjs +1 -1
- package/lib/cli/assistant/model-discovery.mjs +243 -0
- package/lib/cli/assistant/prompt-builder.mjs +2 -5
- package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
- package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
- package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
- package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
- package/lib/cli/assistant/session.mjs +31 -6
- package/lib/cli/assistant/slash-commands.mjs +30 -3
- package/lib/cli/assistant/state.mjs +237 -71
- package/lib/cli/assistant/tool-registry.mjs +325 -39
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/commands/assistant.mjs +4 -3
- package/lib/cli/commands/browser/serve.mjs +5 -23
- package/lib/cli/commands/cleanup.mjs +8 -2
- package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
- package/lib/cli/commands/destroy.mjs +8 -2
- package/lib/cli/commands/discover.mjs +5 -27
- package/lib/cli/commands/doctor.mjs +5 -5
- package/lib/cli/commands/flags.mjs +61 -0
- package/lib/cli/commands/run.mjs +10 -2
- package/lib/cli/commands/status.mjs +10 -2
- package/lib/cli/commands/typecheck.mjs +5 -5
- package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
- package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
- package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
- package/lib/cli/config.mjs +63 -0
- package/lib/cli/operations/browser/serve/operation.mjs +23 -0
- package/lib/cli/operations/cleanup/operation.mjs +8 -0
- package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
- package/lib/cli/operations/destroy/operation.mjs +12 -0
- package/lib/cli/operations/discover/operation.mjs +32 -0
- package/lib/cli/operations/doctor/operation.mjs +5 -0
- package/lib/cli/operations/run/operation.mjs +129 -0
- package/lib/cli/operations/status/operation.mjs +7 -0
- package/lib/cli/operations/typecheck/operation.mjs +5 -0
- package/lib/cli/renderers/browser-serve/text.mjs +6 -0
- package/lib/cli/renderers/cleanup/text.mjs +3 -0
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
- package/lib/cli/renderers/destroy/text.mjs +3 -0
- package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
- package/lib/cli/renderers/discover/text.mjs +7 -0
- package/lib/cli/renderers/doctor/text.mjs +7 -0
- package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
- package/lib/cli/renderers/run/interactive.mjs +119 -0
- package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
- package/lib/cli/renderers/status/text.mjs +7 -0
- package/lib/cli/renderers/typecheck/text.mjs +7 -0
- package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
- package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
- package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
- package/lib/cli/terminal/capabilities.mjs +33 -0
- package/lib/database/index.mjs +9 -21
- package/lib/database/template-steps.mjs +3 -3
- package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
- package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
- package/lib/runner/maintenance.mjs +25 -14
- package/lib/runner/readiness.mjs +5 -4
- package/lib/runner/runtime-preparation.mjs +36 -0
- package/lib/runner/state-io.mjs +10 -4
- package/lib/runner/template.mjs +24 -3
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/assistant/command-plan.mjs +0 -227
- package/lib/cli/command-helpers.mjs +0 -191
- package/lib/cli/presentation/tree-reporter.mjs +0 -96
- package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
- package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
- /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
- /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
- /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
- /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
package/lib/runner/readiness.mjs
CHANGED
|
@@ -96,11 +96,11 @@ export async function isPortInUse({ host, port }) {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export function
|
|
99
|
+
export function buildRunStatusLines(productDir) {
|
|
100
100
|
const manifests = listRunManifests(productDir);
|
|
101
|
-
if (manifests.length === 0) return;
|
|
101
|
+
if (manifests.length === 0) return [];
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
const lines = [" runs/"];
|
|
104
104
|
for (const manifest of manifests) {
|
|
105
105
|
const state = isPidRunning(manifest.pid) ? "active" : "stale";
|
|
106
106
|
const ports = [
|
|
@@ -110,8 +110,9 @@ export function printRunStatus(productDir) {
|
|
|
110
110
|
)
|
|
111
111
|
),
|
|
112
112
|
];
|
|
113
|
-
|
|
113
|
+
lines.push(
|
|
114
114
|
` ${manifest.runId}: ${state} pid=${manifest.pid}${ports.length > 0 ? ` ports=${ports.join(",")}` : ""}`
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
|
+
return lines;
|
|
117
118
|
}
|
|
@@ -127,6 +127,7 @@ export async function computeRuntimePrepareFingerprint(config) {
|
|
|
127
127
|
: null,
|
|
128
128
|
})
|
|
129
129
|
);
|
|
130
|
+
hash.update(JSON.stringify(collectRuntimeDatabaseFingerprintInputs(config)));
|
|
130
131
|
|
|
131
132
|
for (const envFile of config.testkit.envFiles || []) {
|
|
132
133
|
appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile));
|
|
@@ -138,6 +139,41 @@ export async function computeRuntimePrepareFingerprint(config) {
|
|
|
138
139
|
return hash.digest("hex");
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
function collectRuntimeDatabaseFingerprintInputs(config) {
|
|
143
|
+
const inputs = [];
|
|
144
|
+
const ownDatabaseUrl = readDatabaseUrl(config.stateDir);
|
|
145
|
+
if (ownDatabaseUrl) {
|
|
146
|
+
inputs.push({ service: config.name, url: ownDatabaseUrl });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const referencedServices = new Set();
|
|
150
|
+
for (const value of Object.values(config.testkit.serviceEnv || {})) {
|
|
151
|
+
collectDatabasePlaceholderServices(value, referencedServices, config.name);
|
|
152
|
+
}
|
|
153
|
+
for (const value of Object.values(config.testkit.local?.env || {})) {
|
|
154
|
+
collectDatabasePlaceholderServices(value, referencedServices, config.name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const stateDirByService = config.testkit.templateContext?.stateDirByService;
|
|
158
|
+
for (const serviceName of [...referencedServices].sort()) {
|
|
159
|
+
const stateDir = stateDirByService?.get?.(serviceName);
|
|
160
|
+
const databaseUrl = stateDir ? readDatabaseUrl(stateDir) : null;
|
|
161
|
+
inputs.push({ service: serviceName, url: databaseUrl || null });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return inputs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function collectDatabasePlaceholderServices(value, out, defaultServiceName) {
|
|
168
|
+
if (typeof value !== "string") return;
|
|
169
|
+
const matcher = /\{db(?:Url|Host|Port|Name|User|Password)(?::([a-zA-Z0-9_-]+))?\}/g;
|
|
170
|
+
let match = matcher.exec(value);
|
|
171
|
+
while (match) {
|
|
172
|
+
out.add(match[1] || defaultServiceName);
|
|
173
|
+
match = matcher.exec(value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
141
177
|
function appendResolvedInputToHash(hash, productDir, absPath) {
|
|
142
178
|
const relative = path.relative(productDir, absPath);
|
|
143
179
|
appendInputToHash(hash, productDir, relative);
|
package/lib/runner/state-io.mjs
CHANGED
|
@@ -33,16 +33,22 @@ export function readStateValue(filePath) {
|
|
|
33
33
|
return fs.readFileSync(filePath, "utf8").trim();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function
|
|
36
|
+
export function collectStateDirLines(dir, indent = "") {
|
|
37
|
+
const lines = [];
|
|
38
|
+
appendStateDirLines(lines, dir, indent);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendStateDirLines(lines, dir, indent) {
|
|
37
43
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
44
|
const filePath = path.join(dir, entry.name);
|
|
39
45
|
if (entry.isDirectory()) {
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
lines.push(`${indent}${entry.name}/`);
|
|
47
|
+
appendStateDirLines(lines, filePath, `${indent} `);
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
const value =
|
|
45
51
|
entry.name === "password" ? "********" : fs.readFileSync(filePath, "utf8").trim();
|
|
46
|
-
|
|
52
|
+
lines.push(`${indent}${entry.name}: ${value}`);
|
|
47
53
|
}
|
|
48
54
|
}
|
package/lib/runner/template.mjs
CHANGED
|
@@ -204,17 +204,29 @@ export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.en
|
|
|
204
204
|
return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
export function buildTemplateExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
|
|
208
|
+
return buildExecutionEnvWithContext(config, null, extraEnv, processEnv, {
|
|
209
|
+
omitRuntimeDatabaseBindings: true,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
207
213
|
export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
|
|
208
214
|
return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
|
|
217
|
+
function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, options = {}) {
|
|
212
218
|
const inheritedEnv = { ...processEnv };
|
|
213
219
|
const templateContext = buildTemplateContext(config, lease);
|
|
220
|
+
const serviceEnv = options.omitRuntimeDatabaseBindings
|
|
221
|
+
? omitRuntimeDatabaseBindings(config.testkit.serviceEnv || {})
|
|
222
|
+
: config.testkit.serviceEnv || {};
|
|
223
|
+
const localEnv = options.omitRuntimeDatabaseBindings
|
|
224
|
+
? omitRuntimeDatabaseBindings(config.testkit.local?.env || {})
|
|
225
|
+
: config.testkit.local?.env || {};
|
|
214
226
|
const env = {
|
|
215
227
|
...inheritedEnv,
|
|
216
|
-
...resolveEnvTemplates(
|
|
217
|
-
...resolveEnvTemplates(
|
|
228
|
+
...resolveEnvTemplates(serviceEnv, templateContext),
|
|
229
|
+
...resolveEnvTemplates(localEnv, templateContext),
|
|
218
230
|
...resolveEnvTemplates(extraEnv, templateContext),
|
|
219
231
|
TESTKIT_ACTIVE: "1",
|
|
220
232
|
...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
|
|
@@ -340,6 +352,15 @@ function resolveEnvTemplates(values, templateContext) {
|
|
|
340
352
|
);
|
|
341
353
|
}
|
|
342
354
|
|
|
355
|
+
function omitRuntimeDatabaseBindings(values = {}) {
|
|
356
|
+
return Object.fromEntries(
|
|
357
|
+
Object.entries(values).filter(([_key, value]) => {
|
|
358
|
+
if (typeof value !== "string") return true;
|
|
359
|
+
return !/\{db(?:Url|Host|Port|Name|User|Password)(?::[a-zA-Z0-9_-]+)?\}/.test(value);
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
function finalizeRuntimePrepare(prepare, context) {
|
|
344
365
|
if (!prepare) {
|
|
345
366
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.98",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.98"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.98",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@babel/code-frame": "^7.29.0",
|
|
86
|
-
"@elench/next-analysis": "0.1.
|
|
87
|
-
"@elench/testkit-bridge": "0.1.
|
|
88
|
-
"@elench/testkit-protocol": "0.1.
|
|
89
|
-
"@elench/ts-analysis": "0.1.
|
|
86
|
+
"@elench/next-analysis": "0.1.98",
|
|
87
|
+
"@elench/testkit-bridge": "0.1.98",
|
|
88
|
+
"@elench/testkit-protocol": "0.1.98",
|
|
89
|
+
"@elench/ts-analysis": "0.1.98",
|
|
90
90
|
"@oclif/core": "^4.10.6",
|
|
91
91
|
"esbuild": "^0.25.11",
|
|
92
92
|
"execa": "^9.5.0",
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
const TESTKIT_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
2
|
-
const TESTKIT_DIR_COMMANDS = new Set(["run", "discover", "status", "doctor", "destroy", "cleanup", "typecheck"]);
|
|
3
|
-
const PACKAGE_RUNNERS = new Set(["npx", "pnpm", "npm", "yarn", "bun"]);
|
|
4
|
-
|
|
5
|
-
export function extractShellCommand(args = {}) {
|
|
6
|
-
if (!args || typeof args !== "object") return "";
|
|
7
|
-
const value =
|
|
8
|
-
args.command ??
|
|
9
|
-
args.cmd ??
|
|
10
|
-
args.commandString ??
|
|
11
|
-
args.shellCommand ??
|
|
12
|
-
args.input ??
|
|
13
|
-
args.script ??
|
|
14
|
-
"";
|
|
15
|
-
if (Array.isArray(value)) return value.map((part) => shellEscapeArg(part)).join(" ");
|
|
16
|
-
return String(value || "");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function planShellCommand(rawCommand) {
|
|
20
|
-
const raw = String(rawCommand || "").trim();
|
|
21
|
-
if (!raw) {
|
|
22
|
-
return {
|
|
23
|
-
executableCommand: "",
|
|
24
|
-
rawCommand: raw,
|
|
25
|
-
displayCommand: raw,
|
|
26
|
-
command: "",
|
|
27
|
-
title: "Shell command",
|
|
28
|
-
testkitRelated: false,
|
|
29
|
-
normalized: false,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const testkit = planTestkitCommand(raw);
|
|
34
|
-
if (testkit) return testkit;
|
|
35
|
-
|
|
36
|
-
const testkitScript = planTestkitPackageScript(raw);
|
|
37
|
-
if (testkitScript) return testkitScript;
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
executableCommand: raw,
|
|
41
|
-
rawCommand: raw,
|
|
42
|
-
displayCommand: raw,
|
|
43
|
-
command: firstCommandToken(raw),
|
|
44
|
-
title: "Shell command",
|
|
45
|
-
testkitRelated: false,
|
|
46
|
-
normalized: false,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function planTestkitPackageScript(raw) {
|
|
51
|
-
if (containsShellControl(raw)) return null;
|
|
52
|
-
const tokens = tokenizeShellWords(raw);
|
|
53
|
-
if (!tokens || tokens.length < 3) return null;
|
|
54
|
-
if (tokens[0] !== "npm" || tokens[1] !== "run") return null;
|
|
55
|
-
if (tokens[2] !== "testkit" && !tokens[2].startsWith("testkit:")) return null;
|
|
56
|
-
return {
|
|
57
|
-
executableCommand: raw,
|
|
58
|
-
rawCommand: raw,
|
|
59
|
-
displayCommand: raw,
|
|
60
|
-
command: "npm run testkit",
|
|
61
|
-
title: "npm testkit script",
|
|
62
|
-
testkitRelated: true,
|
|
63
|
-
normalized: false,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function planTestkitCommand(raw) {
|
|
68
|
-
if (containsShellControl(raw)) return null;
|
|
69
|
-
const tokens = tokenizeShellWords(raw);
|
|
70
|
-
if (!tokens || tokens.length === 0) return null;
|
|
71
|
-
|
|
72
|
-
const extracted = extractTestkitInvocation(tokens);
|
|
73
|
-
if (!extracted) return null;
|
|
74
|
-
|
|
75
|
-
const canonicalArgs = canonicalizeTestkitArgs(extracted.args);
|
|
76
|
-
const executableCommand = ["testkit", ...canonicalArgs].map(shellEscapeArg).join(" ");
|
|
77
|
-
const wasNormalized = executableCommand !== raw;
|
|
78
|
-
return {
|
|
79
|
-
executableCommand,
|
|
80
|
-
rawCommand: raw,
|
|
81
|
-
displayCommand: executableCommand,
|
|
82
|
-
command: "testkit",
|
|
83
|
-
title: "testkit command",
|
|
84
|
-
testkitRelated: true,
|
|
85
|
-
normalized: wasNormalized,
|
|
86
|
-
normalizationReason: wasNormalized ? extracted.reason : null,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function extractTestkitInvocation(tokens) {
|
|
91
|
-
if (tokens[0] === "testkit") {
|
|
92
|
-
return {
|
|
93
|
-
args: tokens.slice(1),
|
|
94
|
-
reason: "canonicalized local testkit invocation",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!PACKAGE_RUNNERS.has(tokens[0])) return null;
|
|
99
|
-
|
|
100
|
-
if (tokens[0] === "npm" && ["exec", "x"].includes(tokens[1])) {
|
|
101
|
-
const index = findPackageTarget(tokens, 2);
|
|
102
|
-
if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced npm exec testkit with local testkit" };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (tokens[0] === "npx") {
|
|
106
|
-
const index = findPackageTarget(tokens, 1);
|
|
107
|
-
if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced npx testkit with local testkit" };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (tokens[0] === "pnpm" && ["exec", "dlx"].includes(tokens[1])) {
|
|
111
|
-
const index = findPackageTarget(tokens, 2);
|
|
112
|
-
if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced pnpm testkit launcher with local testkit" };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (tokens[0] === "yarn" && tokens[1] === "testkit") {
|
|
116
|
-
return { args: tokens.slice(2), reason: "replaced yarn testkit launcher with local testkit" };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (tokens[0] === "bun" && ["x", "run"].includes(tokens[1])) {
|
|
120
|
-
const index = findPackageTarget(tokens, 2);
|
|
121
|
-
if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced bun testkit launcher with local testkit" };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function canonicalizeTestkitArgs(inputArgs) {
|
|
128
|
-
const args = [...inputArgs];
|
|
129
|
-
if (args.length === 0) return [];
|
|
130
|
-
|
|
131
|
-
if (TESTKIT_TYPES.has(args[0])) {
|
|
132
|
-
return withDir(["run", "--type", args[0], ...args.slice(1)]);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!TESTKIT_DIR_COMMANDS.has(args[0])) {
|
|
136
|
-
return args;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (args[0] === "run") {
|
|
140
|
-
const runArgs = [...args];
|
|
141
|
-
if (TESTKIT_TYPES.has(runArgs[1])) {
|
|
142
|
-
const type = runArgs.splice(1, 1)[0];
|
|
143
|
-
if (!hasFlag(runArgs, "--type", "-t")) runArgs.splice(1, 0, "--type", type);
|
|
144
|
-
}
|
|
145
|
-
return withDir(runArgs);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return withDir(args);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function withDir(args) {
|
|
152
|
-
if (hasFlag(args, "--dir", "-d") || args.includes("--help") || args.includes("-h")) return args;
|
|
153
|
-
const [command, ...rest] = args;
|
|
154
|
-
return [command, "--dir", ".", ...rest];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function hasFlag(args, longFlag, shortFlag) {
|
|
158
|
-
return args.some((arg) => arg === longFlag || arg.startsWith(`${longFlag}=`) || arg === shortFlag);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function findPackageTarget(tokens, startIndex) {
|
|
162
|
-
for (let index = startIndex; index < tokens.length; index += 1) {
|
|
163
|
-
const token = tokens[index];
|
|
164
|
-
if (token === "--") continue;
|
|
165
|
-
if (token === "testkit" || token === "@elench/testkit") return index;
|
|
166
|
-
if (!token.startsWith("-")) return -1;
|
|
167
|
-
}
|
|
168
|
-
return -1;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function firstCommandToken(command) {
|
|
172
|
-
const tokens = tokenizeShellWords(command);
|
|
173
|
-
return tokens?.[0] || command.split(/\s+/)[0] || "command";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function containsShellControl(command) {
|
|
177
|
-
return /[\n;&|<>`]/.test(command);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function tokenizeShellWords(command) {
|
|
181
|
-
const words = [];
|
|
182
|
-
let current = "";
|
|
183
|
-
let quote = null;
|
|
184
|
-
let escaping = false;
|
|
185
|
-
|
|
186
|
-
for (const char of String(command)) {
|
|
187
|
-
if (escaping) {
|
|
188
|
-
current += char;
|
|
189
|
-
escaping = false;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (char === "\\") {
|
|
193
|
-
escaping = true;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (quote) {
|
|
197
|
-
if (char === quote) {
|
|
198
|
-
quote = null;
|
|
199
|
-
} else {
|
|
200
|
-
current += char;
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (char === "'" || char === '"') {
|
|
205
|
-
quote = char;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (/\s/.test(char)) {
|
|
209
|
-
if (current) {
|
|
210
|
-
words.push(current);
|
|
211
|
-
current = "";
|
|
212
|
-
}
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
current += char;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (escaping || quote) return null;
|
|
219
|
-
if (current) words.push(current);
|
|
220
|
-
return words;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function shellEscapeArg(value) {
|
|
224
|
-
const stringValue = String(value);
|
|
225
|
-
if (/^[a-zA-Z0-9._:@/%+=,-]+$/.test(stringValue)) return stringValue;
|
|
226
|
-
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
|
|
227
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Flags } from "@oclif/core";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { loadManagedConfigs } from "../app/configs.mjs";
|
|
4
|
-
import {
|
|
5
|
-
parseFileTimeoutOption,
|
|
6
|
-
parseShardOption,
|
|
7
|
-
parseSuiteOption,
|
|
8
|
-
parseTypeOption,
|
|
9
|
-
parseWorkersOption,
|
|
10
|
-
resolveRequestedFiles,
|
|
11
|
-
} from "./args.mjs";
|
|
12
|
-
import * as runner from "../runner/index.mjs";
|
|
13
|
-
import { createRunReporter } from "./presentation/run-reporter.mjs";
|
|
14
|
-
import { createTreeReporter } from "./presentation/tree-reporter.mjs";
|
|
15
|
-
import { createRunEventsReporter } from "./presentation/events-reporter.mjs";
|
|
16
|
-
|
|
17
|
-
export const sharedFlags = {
|
|
18
|
-
dir: Flags.string({
|
|
19
|
-
description: "Explicit product directory",
|
|
20
|
-
}),
|
|
21
|
-
service: Flags.string({
|
|
22
|
-
description: "Limit the operation or assistant context to one service",
|
|
23
|
-
}),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const runFlags = {
|
|
27
|
-
...sharedFlags,
|
|
28
|
-
type: Flags.string({
|
|
29
|
-
char: "t",
|
|
30
|
-
multiple: true,
|
|
31
|
-
description: "Run specific suite type(s): int, e2e, scenario, dal, load, pw, all",
|
|
32
|
-
}),
|
|
33
|
-
suite: Flags.string({
|
|
34
|
-
char: "s",
|
|
35
|
-
multiple: true,
|
|
36
|
-
description: "Run specific suite(s)",
|
|
37
|
-
}),
|
|
38
|
-
file: Flags.string({
|
|
39
|
-
char: "f",
|
|
40
|
-
multiple: true,
|
|
41
|
-
description: "Run specific file(s)",
|
|
42
|
-
}),
|
|
43
|
-
workers: Flags.string({
|
|
44
|
-
description: "Number of test executors for the whole run",
|
|
45
|
-
}),
|
|
46
|
-
"file-timeout-seconds": Flags.string({
|
|
47
|
-
description: "Per-file wall-clock timeout in seconds",
|
|
48
|
-
}),
|
|
49
|
-
shard: Flags.string({
|
|
50
|
-
description: "Run only shard i of n at suite granularity",
|
|
51
|
-
}),
|
|
52
|
-
seed: Flags.string({
|
|
53
|
-
description: "Deterministic seed for scenario suites",
|
|
54
|
-
}),
|
|
55
|
-
"write-status": Flags.boolean({
|
|
56
|
-
description: "Write a deterministic testkit.status.json snapshot",
|
|
57
|
-
default: false,
|
|
58
|
-
}),
|
|
59
|
-
"allow-partial-status": Flags.boolean({
|
|
60
|
-
description: "Allow --write-status for filtered runs",
|
|
61
|
-
default: false,
|
|
62
|
-
}),
|
|
63
|
-
"ignore-skip-rules": Flags.boolean({
|
|
64
|
-
description: "Run files even if testkit.config.ts marks them skipped",
|
|
65
|
-
default: false,
|
|
66
|
-
}),
|
|
67
|
-
"output-mode": Flags.string({
|
|
68
|
-
description: "Reporter mode",
|
|
69
|
-
options: ["compact", "debug", "events"],
|
|
70
|
-
}),
|
|
71
|
-
debug: Flags.boolean({
|
|
72
|
-
description: "Alias for --output-mode debug",
|
|
73
|
-
default: false,
|
|
74
|
-
}),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export async function resolveConfigsForCommand(flags) {
|
|
78
|
-
return loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function executeRunCommand(command, flags, positionalType = null) {
|
|
82
|
-
const request = await buildRunRequest(flags, positionalType, process.cwd(), process.cwd());
|
|
83
|
-
const { allConfigs, configs, typeValues, suiteSelectors, productDir } = request;
|
|
84
|
-
const outputMode = command.jsonEnabled()
|
|
85
|
-
? "json"
|
|
86
|
-
: flags.debug
|
|
87
|
-
? "debug"
|
|
88
|
-
: flags["output-mode"] || "compact";
|
|
89
|
-
|
|
90
|
-
let reporter;
|
|
91
|
-
let finalize = Promise.resolve();
|
|
92
|
-
let close = () => {};
|
|
93
|
-
|
|
94
|
-
if (outputMode === "compact" && process.stdout.isTTY) {
|
|
95
|
-
const tree = createTreeReporter({
|
|
96
|
-
stdout: process.stdout,
|
|
97
|
-
stderr: process.stderr,
|
|
98
|
-
productDir,
|
|
99
|
-
});
|
|
100
|
-
reporter = tree.reporter;
|
|
101
|
-
finalize = tree.finalize;
|
|
102
|
-
close = tree.close;
|
|
103
|
-
} else if (outputMode === "events") {
|
|
104
|
-
reporter = createRunEventsReporter({ stdout: process.stdout, stderr: process.stderr });
|
|
105
|
-
} else {
|
|
106
|
-
reporter = createRunReporter({ outputMode });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const result = await runner.runAll(
|
|
111
|
-
configs,
|
|
112
|
-
typeValues,
|
|
113
|
-
suiteSelectors,
|
|
114
|
-
{
|
|
115
|
-
reporter,
|
|
116
|
-
...request.runOptions,
|
|
117
|
-
},
|
|
118
|
-
allConfigs
|
|
119
|
-
);
|
|
120
|
-
await finalize;
|
|
121
|
-
return {
|
|
122
|
-
outputMode,
|
|
123
|
-
...result,
|
|
124
|
-
};
|
|
125
|
-
} catch (error) {
|
|
126
|
-
close();
|
|
127
|
-
await finalize.catch(() => {});
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function buildRunRequest(flags, positionalType = null, cwd = process.cwd(), invocationCwd = process.cwd()) {
|
|
133
|
-
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
134
|
-
const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
|
|
135
|
-
const fileTimeoutSeconds =
|
|
136
|
-
flags["file-timeout-seconds"] == null
|
|
137
|
-
? null
|
|
138
|
-
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
139
|
-
const shard = parseShardOption(flags.shard);
|
|
140
|
-
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
141
|
-
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
142
|
-
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
143
|
-
const productDir = allConfigs[0]?.productDir || cwd;
|
|
144
|
-
const fileNames = resolveRequestedFiles(rawFileNames, productDir, invocationCwd);
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
allConfigs,
|
|
148
|
-
configs,
|
|
149
|
-
productDir,
|
|
150
|
-
typeValues,
|
|
151
|
-
suiteSelectors,
|
|
152
|
-
runOptions: {
|
|
153
|
-
...flags,
|
|
154
|
-
typeValues,
|
|
155
|
-
fileNames,
|
|
156
|
-
workers,
|
|
157
|
-
fileTimeoutSeconds,
|
|
158
|
-
shard,
|
|
159
|
-
scenarioSeed: flags.seed || null,
|
|
160
|
-
serviceFilter: flags.service || null,
|
|
161
|
-
writeStatus: flags["write-status"],
|
|
162
|
-
allowPartialStatus: flags["allow-partial-status"],
|
|
163
|
-
ignoreSkipRules: flags["ignore-skip-rules"],
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export async function runStatusLike(commandName, flags) {
|
|
169
|
-
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
170
|
-
|
|
171
|
-
if (commandName === "cleanup") {
|
|
172
|
-
await runner.cleanup(allConfigs[0]?.productDir || process.cwd());
|
|
173
|
-
return { ok: true };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const productResults = [];
|
|
177
|
-
for (const config of configs) {
|
|
178
|
-
if (commandName === "status") {
|
|
179
|
-
productResults.push(runner.showStatus(config));
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
await runner.destroy(config);
|
|
183
|
-
productResults.push({ name: config.name, destroyed: true });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { ok: true, results: productResults };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function relativeToProduct(productDir, targetPath) {
|
|
190
|
-
return path.relative(productDir, targetPath);
|
|
191
|
-
}
|