@boolesai/tspec-cli 1.0.0 → 1.2.0
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/README.md +21 -21
- package/dist/index.js +227 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/utils/files.d.ts +43 -4
- package/types/utils/formatter.d.ts +1 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npx @boolesai/tspec-cli <command>
|
|
|
21
21
|
|
|
22
22
|
### `tspec validate`
|
|
23
23
|
|
|
24
|
-
Validate `.
|
|
24
|
+
Validate `.tcase` files for schema correctness.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
tspec validate <files...> [options]
|
|
@@ -34,13 +34,13 @@ tspec validate <files...> [options]
|
|
|
34
34
|
**Examples:**
|
|
35
35
|
```bash
|
|
36
36
|
# Validate a single file
|
|
37
|
-
tspec validate tests/login.http.
|
|
37
|
+
tspec validate tests/login.http.tcase
|
|
38
38
|
|
|
39
39
|
# Validate multiple files with glob pattern
|
|
40
|
-
tspec validate "tests/**/*.
|
|
40
|
+
tspec validate "tests/**/*.tcase"
|
|
41
41
|
|
|
42
42
|
# JSON output for CI/CD
|
|
43
|
-
tspec validate tests/*.
|
|
43
|
+
tspec validate tests/*.tcase --output json
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
### `tspec run`
|
|
@@ -63,25 +63,25 @@ tspec run <files...> [options]
|
|
|
63
63
|
**Examples:**
|
|
64
64
|
```bash
|
|
65
65
|
# Run tests with default settings
|
|
66
|
-
tspec run tests/*.http.
|
|
66
|
+
tspec run tests/*.http.tcase
|
|
67
67
|
|
|
68
68
|
# Run with environment variables
|
|
69
|
-
tspec run tests/*.
|
|
69
|
+
tspec run tests/*.tcase -e API_HOST=api.example.com -e API_KEY=secret
|
|
70
70
|
|
|
71
71
|
# Run with parameters
|
|
72
|
-
tspec run tests/*.
|
|
72
|
+
tspec run tests/*.tcase -p username=testuser -p timeout=5000
|
|
73
73
|
|
|
74
74
|
# Run with higher concurrency
|
|
75
|
-
tspec run tests/*.
|
|
75
|
+
tspec run tests/*.tcase -c 10
|
|
76
76
|
|
|
77
77
|
# Verbose output for debugging
|
|
78
|
-
tspec run tests/*.
|
|
78
|
+
tspec run tests/*.tcase -v
|
|
79
79
|
|
|
80
80
|
# JSON output for CI/CD
|
|
81
|
-
tspec run tests/*.
|
|
81
|
+
tspec run tests/*.tcase --output json
|
|
82
82
|
|
|
83
83
|
# Stop on first failure
|
|
84
|
-
tspec run tests/*.
|
|
84
|
+
tspec run tests/*.tcase --fail-fast
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
### `tspec parse`
|
|
@@ -102,13 +102,13 @@ tspec parse <files...> [options]
|
|
|
102
102
|
**Examples:**
|
|
103
103
|
```bash
|
|
104
104
|
# Parse and display test cases
|
|
105
|
-
tspec parse tests/login.http.
|
|
105
|
+
tspec parse tests/login.http.tcase
|
|
106
106
|
|
|
107
107
|
# JSON output for inspection
|
|
108
|
-
tspec parse tests/*.
|
|
108
|
+
tspec parse tests/*.tcase --output json
|
|
109
109
|
|
|
110
110
|
# With variable substitution
|
|
111
|
-
tspec parse tests/*.
|
|
111
|
+
tspec parse tests/*.tcase -e API_HOST=localhost
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
### `tspec list`
|
|
@@ -150,7 +150,7 @@ TSpec CLI can run as an MCP server, exposing all commands as tools for AI assist
|
|
|
150
150
|
| Tool | Description |
|
|
151
151
|
|------|-------------|
|
|
152
152
|
| `tspec_run` | Execute test cases and report results |
|
|
153
|
-
| `tspec_validate` | Validate .
|
|
153
|
+
| `tspec_validate` | Validate .tcase files for schema correctness |
|
|
154
154
|
| `tspec_parse` | Parse and display test case information |
|
|
155
155
|
| `tspec_list` | List supported protocols |
|
|
156
156
|
|
|
@@ -191,7 +191,7 @@ Or if installed globally:
|
|
|
191
191
|
|
|
192
192
|
```json
|
|
193
193
|
{
|
|
194
|
-
"files": ["tests/*.
|
|
194
|
+
"files": ["tests/*.tcase"],
|
|
195
195
|
"concurrency": 5,
|
|
196
196
|
"env": { "API_HOST": "localhost" },
|
|
197
197
|
"params": { "timeout": "5000" },
|
|
@@ -204,7 +204,7 @@ Or if installed globally:
|
|
|
204
204
|
|
|
205
205
|
```json
|
|
206
206
|
{
|
|
207
|
-
"files": ["tests/*.
|
|
207
|
+
"files": ["tests/*.tcase"],
|
|
208
208
|
"output": "text"
|
|
209
209
|
}
|
|
210
210
|
```
|
|
@@ -213,7 +213,7 @@ Or if installed globally:
|
|
|
213
213
|
|
|
214
214
|
```json
|
|
215
215
|
{
|
|
216
|
-
"files": ["tests/*.
|
|
216
|
+
"files": ["tests/*.tcase"],
|
|
217
217
|
"env": { "API_HOST": "localhost" },
|
|
218
218
|
"params": { "timeout": "5000" },
|
|
219
219
|
"verbose": true,
|
|
@@ -244,10 +244,10 @@ Or if installed globally:
|
|
|
244
244
|
```yaml
|
|
245
245
|
- name: Run TSpec tests
|
|
246
246
|
run: |
|
|
247
|
-
npx @boolesai/tspec-cli run tests/*.
|
|
247
|
+
npx @boolesai/tspec-cli run tests/*.tcase --output json > results.json
|
|
248
248
|
|
|
249
249
|
- name: Validate TSpec files
|
|
250
|
-
run: npx @boolesai/tspec-cli validate tests/*.
|
|
250
|
+
run: npx @boolesai/tspec-cli validate tests/*.tcase
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
### GitLab CI
|
|
@@ -255,7 +255,7 @@ Or if installed globally:
|
|
|
255
255
|
```yaml
|
|
256
256
|
test:
|
|
257
257
|
script:
|
|
258
|
-
- npx @boolesai/tspec-cli run tests/*.
|
|
258
|
+
- npx @boolesai/tspec-cli run tests/*.tcase --output json
|
|
259
259
|
artifacts:
|
|
260
260
|
reports:
|
|
261
261
|
junit: results.json
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { existsSync, statSync, readFileSync } from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { isAbsolute, resolve as resolve$1, basename, relative, dirname, join } from "path";
|
|
2
5
|
import ora from "ora";
|
|
3
|
-
import { getTypeFromFilePath, validateTestCase, clearTemplateCache, parseTestCases, scheduler, registry as registry$1 } from "@boolesai/tspec";
|
|
6
|
+
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, validateTestCase, clearTemplateCache, executeSuite, parseTestCases, scheduler, registry as registry$1 } from "@boolesai/tspec";
|
|
4
7
|
import { glob } from "glob";
|
|
5
|
-
import { isAbsolute, resolve as resolve$1, basename, relative } from "path";
|
|
6
|
-
import { existsSync, statSync } from "fs";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import process$2 from "node:process";
|
|
9
10
|
async function discoverTSpecFiles(patterns, cwd) {
|
|
@@ -15,14 +16,14 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
15
16
|
if (existsSync(absolutePath)) {
|
|
16
17
|
const stat = statSync(absolutePath);
|
|
17
18
|
if (stat.isFile()) {
|
|
18
|
-
if (absolutePath.endsWith(".
|
|
19
|
+
if (absolutePath.endsWith(".tcase")) {
|
|
19
20
|
filePaths.push(absolutePath);
|
|
20
21
|
}
|
|
21
22
|
continue;
|
|
22
23
|
} else if (stat.isDirectory()) {
|
|
23
|
-
const dirFiles = await glob("**/*.
|
|
24
|
+
const dirFiles = await glob("**/*.tcase", { cwd: absolutePath, absolute: true });
|
|
24
25
|
if (dirFiles.length === 0) {
|
|
25
|
-
errors2.push(`No .
|
|
26
|
+
errors2.push(`No .tcase files found in directory: ${pattern2}`);
|
|
26
27
|
} else {
|
|
27
28
|
filePaths.push(...dirFiles);
|
|
28
29
|
}
|
|
@@ -30,9 +31,9 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
const matches = await glob(pattern2, { cwd: workingDir, absolute: true });
|
|
33
|
-
const tspecMatches = matches.filter((f) => f.endsWith(".
|
|
34
|
+
const tspecMatches = matches.filter((f) => f.endsWith(".tcase"));
|
|
34
35
|
if (tspecMatches.length === 0) {
|
|
35
|
-
errors2.push(`No .
|
|
36
|
+
errors2.push(`No .tcase files matched pattern: ${pattern2}`);
|
|
36
37
|
} else {
|
|
37
38
|
filePaths.push(...tspecMatches);
|
|
38
39
|
}
|
|
@@ -46,6 +47,61 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
46
47
|
}));
|
|
47
48
|
return { files, errors: errors2 };
|
|
48
49
|
}
|
|
50
|
+
async function discoverAllTestFiles(patterns, cwd) {
|
|
51
|
+
const workingDir = process.cwd();
|
|
52
|
+
const tspecPaths = [];
|
|
53
|
+
const suitePaths = [];
|
|
54
|
+
const errors2 = [];
|
|
55
|
+
for (const pattern2 of patterns) {
|
|
56
|
+
const absolutePath = isAbsolute(pattern2) ? pattern2 : resolve$1(workingDir, pattern2);
|
|
57
|
+
if (existsSync(absolutePath)) {
|
|
58
|
+
const stat = statSync(absolutePath);
|
|
59
|
+
if (stat.isFile()) {
|
|
60
|
+
if (absolutePath.endsWith(".tcase")) {
|
|
61
|
+
tspecPaths.push(absolutePath);
|
|
62
|
+
} else if (isSuiteFile(absolutePath)) {
|
|
63
|
+
suitePaths.push(absolutePath);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
} else if (stat.isDirectory()) {
|
|
67
|
+
const tspecFiles2 = await glob("**/*.tcase", { cwd: absolutePath, absolute: true });
|
|
68
|
+
const suiteFiles2 = await glob("**/*.tsuite", { cwd: absolutePath, absolute: true });
|
|
69
|
+
if (tspecFiles2.length === 0 && suiteFiles2.length === 0) {
|
|
70
|
+
errors2.push(`No .tcase or .tsuite files found in directory: ${pattern2}`);
|
|
71
|
+
} else {
|
|
72
|
+
tspecPaths.push(...tspecFiles2);
|
|
73
|
+
suitePaths.push(...suiteFiles2);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const matches = await glob(pattern2, { cwd: workingDir, absolute: true });
|
|
79
|
+
const tspecMatches = matches.filter((f) => f.endsWith(".tcase"));
|
|
80
|
+
const suiteMatches = matches.filter((f) => isSuiteFile(f));
|
|
81
|
+
if (tspecMatches.length === 0 && suiteMatches.length === 0) {
|
|
82
|
+
errors2.push(`No .tcase or .tsuite files matched pattern: ${pattern2}`);
|
|
83
|
+
} else {
|
|
84
|
+
tspecPaths.push(...tspecMatches);
|
|
85
|
+
suitePaths.push(...suiteMatches);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const uniqueTspecPaths = [...new Set(tspecPaths)];
|
|
89
|
+
const uniqueSuitePaths = [...new Set(suitePaths)];
|
|
90
|
+
const tspecFiles = uniqueTspecPaths.map((filePath) => ({
|
|
91
|
+
path: filePath,
|
|
92
|
+
relativePath: relative(workingDir, filePath),
|
|
93
|
+
fileName: basename(filePath),
|
|
94
|
+
protocol: getTypeFromFilePath(filePath)
|
|
95
|
+
}));
|
|
96
|
+
const suiteFiles = uniqueSuitePaths.map((filePath) => ({
|
|
97
|
+
path: filePath,
|
|
98
|
+
relativePath: relative(workingDir, filePath),
|
|
99
|
+
fileName: basename(filePath),
|
|
100
|
+
protocol: getSuiteProtocolType(filePath),
|
|
101
|
+
isTemplate: filePath.endsWith(".tsuite.yaml")
|
|
102
|
+
}));
|
|
103
|
+
return { tspecFiles, suiteFiles, errors: errors2 };
|
|
104
|
+
}
|
|
49
105
|
function formatJson(data) {
|
|
50
106
|
return JSON.stringify(data, null, 2);
|
|
51
107
|
}
|
|
@@ -86,6 +142,13 @@ function formatTestResult(result, verbose = false) {
|
|
|
86
142
|
output += "\n" + assertionLines.join("\n");
|
|
87
143
|
}
|
|
88
144
|
}
|
|
145
|
+
if (verbose && result.extracted && Object.keys(result.extracted).length > 0) {
|
|
146
|
+
output += "\n" + chalk.gray(" Extracted:");
|
|
147
|
+
for (const [key, value] of Object.entries(result.extracted)) {
|
|
148
|
+
output += `
|
|
149
|
+
${key}: ${JSON.stringify(value)}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
89
152
|
return output;
|
|
90
153
|
}
|
|
91
154
|
function formatTestSummary(summary) {
|
|
@@ -131,7 +194,16 @@ function formatParsedTestCase(testCase, options = {}) {
|
|
|
131
194
|
const lines = [];
|
|
132
195
|
lines.push(chalk.bold(`Test Case: ${tc.id || "unknown"}`));
|
|
133
196
|
if (tc.description) lines.push(` Description: ${tc.description}`);
|
|
134
|
-
if (tc.
|
|
197
|
+
if (tc.protocol) lines.push(` Protocol: ${tc.protocol}`);
|
|
198
|
+
if (tc.lifecycle) {
|
|
199
|
+
const lifecycle = tc.lifecycle;
|
|
200
|
+
if (lifecycle.setup?.length) {
|
|
201
|
+
lines.push(` Lifecycle Setup: ${lifecycle.setup.length} action(s)`);
|
|
202
|
+
}
|
|
203
|
+
if (lifecycle.teardown?.length) {
|
|
204
|
+
lines.push(` Lifecycle Teardown: ${lifecycle.teardown.length} action(s)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
135
207
|
return lines.join("\n");
|
|
136
208
|
}
|
|
137
209
|
let globalOptions = {};
|
|
@@ -183,7 +255,7 @@ async function executeValidate(params) {
|
|
|
183
255
|
if (fileDescriptors.length === 0) {
|
|
184
256
|
return {
|
|
185
257
|
success: false,
|
|
186
|
-
output: "No .
|
|
258
|
+
output: "No .tcase files found",
|
|
187
259
|
data: { results: [] }
|
|
188
260
|
};
|
|
189
261
|
}
|
|
@@ -206,7 +278,7 @@ async function executeValidate(params) {
|
|
|
206
278
|
data: { results }
|
|
207
279
|
};
|
|
208
280
|
}
|
|
209
|
-
const validateCommand = new Command("validate").description("Validate .
|
|
281
|
+
const validateCommand = new Command("validate").description("Validate .tcase files for schema correctness").argument("<files...>", "Files or glob patterns to validate").option("-o, --output <format>", "Output format: json, text", "text").option("-q, --quiet", "Only output errors").action(async (files, options) => {
|
|
210
282
|
setLoggerOptions({ quiet: options.quiet });
|
|
211
283
|
const spinner = options.quiet ? null : ora("Validating...").start();
|
|
212
284
|
try {
|
|
@@ -236,6 +308,7 @@ function formatResult(result) {
|
|
|
236
308
|
testCaseId: result.testCaseId,
|
|
237
309
|
passed: result.passed,
|
|
238
310
|
duration: result.duration,
|
|
311
|
+
extracted: Object.keys(result.extracted).length > 0 ? result.extracted : void 0,
|
|
239
312
|
assertions: result.assertions.map((a) => ({
|
|
240
313
|
passed: a.passed,
|
|
241
314
|
type: a.type,
|
|
@@ -287,11 +360,11 @@ async function executeRun(params) {
|
|
|
287
360
|
const output = params.output ?? "text";
|
|
288
361
|
const verbose = params.verbose ?? false;
|
|
289
362
|
const quiet = params.quiet ?? false;
|
|
290
|
-
const {
|
|
291
|
-
if (
|
|
363
|
+
const { tspecFiles, suiteFiles, errors: resolveErrors } = await discoverAllTestFiles(params.files);
|
|
364
|
+
if (tspecFiles.length === 0 && suiteFiles.length === 0) {
|
|
292
365
|
return {
|
|
293
366
|
success: false,
|
|
294
|
-
output: "No .
|
|
367
|
+
output: "No .tcase or .tsuite files found",
|
|
295
368
|
data: {
|
|
296
369
|
results: [],
|
|
297
370
|
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
@@ -299,6 +372,29 @@ async function executeRun(params) {
|
|
|
299
372
|
}
|
|
300
373
|
};
|
|
301
374
|
}
|
|
375
|
+
if (suiteFiles.length > 0) {
|
|
376
|
+
return executeSuiteRun(suiteFiles, tspecFiles, {
|
|
377
|
+
env,
|
|
378
|
+
params: paramValues,
|
|
379
|
+
concurrency,
|
|
380
|
+
failFast,
|
|
381
|
+
output,
|
|
382
|
+
verbose,
|
|
383
|
+
quiet
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return executeTspecRun(tspecFiles, {
|
|
387
|
+
env,
|
|
388
|
+
params: paramValues,
|
|
389
|
+
concurrency,
|
|
390
|
+
failFast,
|
|
391
|
+
output,
|
|
392
|
+
verbose,
|
|
393
|
+
quiet
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async function executeTspecRun(fileDescriptors, options) {
|
|
397
|
+
const { env, params: paramValues, concurrency, failFast, output, verbose, quiet } = options;
|
|
302
398
|
const allResults = [];
|
|
303
399
|
const parseErrors = [];
|
|
304
400
|
let totalTestCases = 0;
|
|
@@ -367,6 +463,116 @@ ${parseErrors.length} file(s) failed to parse:`);
|
|
|
367
463
|
data: { results: formattedResults, summary, parseErrors }
|
|
368
464
|
};
|
|
369
465
|
}
|
|
466
|
+
async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
|
|
467
|
+
const { env, params: paramValues, failFast, output, verbose, quiet } = options;
|
|
468
|
+
const allResults = [];
|
|
469
|
+
const parseErrors = [];
|
|
470
|
+
let totalTests = 0;
|
|
471
|
+
let totalPassed = 0;
|
|
472
|
+
let totalFailed = 0;
|
|
473
|
+
let stopped = false;
|
|
474
|
+
for (const suiteDescriptor of suiteFiles) {
|
|
475
|
+
if (stopped) break;
|
|
476
|
+
if (suiteDescriptor.isTemplate) continue;
|
|
477
|
+
try {
|
|
478
|
+
const suiteResult = await executeSuite(suiteDescriptor.path, {
|
|
479
|
+
env,
|
|
480
|
+
params: paramValues,
|
|
481
|
+
onSuiteStart: (name) => {
|
|
482
|
+
if (!quiet) logger.log(chalk.blue(`
|
|
483
|
+
Suite: ${name}`));
|
|
484
|
+
},
|
|
485
|
+
onTestStart: (file) => {
|
|
486
|
+
if (verbose) logger.log(chalk.gray(` Running: ${file}`));
|
|
487
|
+
},
|
|
488
|
+
onTestComplete: (file, result) => {
|
|
489
|
+
const statusIcon = result.status === "passed" ? chalk.green("✓") : chalk.red("✗");
|
|
490
|
+
if (!quiet) logger.log(` ${statusIcon} ${result.name} (${result.duration}ms)`);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
totalTests += suiteResult.stats.total;
|
|
494
|
+
totalPassed += suiteResult.stats.passed;
|
|
495
|
+
totalFailed += suiteResult.stats.failed + suiteResult.stats.error;
|
|
496
|
+
for (const testResult of suiteResult.tests) {
|
|
497
|
+
allResults.push({
|
|
498
|
+
testCaseId: testResult.name,
|
|
499
|
+
passed: testResult.status === "passed",
|
|
500
|
+
duration: testResult.duration,
|
|
501
|
+
assertions: (testResult.assertions || []).map((a) => ({
|
|
502
|
+
passed: a.passed,
|
|
503
|
+
type: a.type,
|
|
504
|
+
message: a.message || ""
|
|
505
|
+
}))
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (suiteResult.suites) {
|
|
509
|
+
for (const nestedSuite of suiteResult.suites) {
|
|
510
|
+
totalTests += nestedSuite.stats.total;
|
|
511
|
+
totalPassed += nestedSuite.stats.passed;
|
|
512
|
+
totalFailed += nestedSuite.stats.failed + nestedSuite.stats.error;
|
|
513
|
+
for (const testResult of nestedSuite.tests) {
|
|
514
|
+
allResults.push({
|
|
515
|
+
testCaseId: `${nestedSuite.name}/${testResult.name}`,
|
|
516
|
+
passed: testResult.status === "passed",
|
|
517
|
+
duration: testResult.duration,
|
|
518
|
+
assertions: (testResult.assertions || []).map((a) => ({
|
|
519
|
+
passed: a.passed,
|
|
520
|
+
type: a.type,
|
|
521
|
+
message: a.message || ""
|
|
522
|
+
}))
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (failFast && (suiteResult.status === "failed" || suiteResult.status === "error")) {
|
|
528
|
+
stopped = true;
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
parseErrors.push({
|
|
532
|
+
file: suiteDescriptor.path,
|
|
533
|
+
error: err instanceof Error ? err.message : String(err)
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (additionalTspecFiles.length > 0 && !stopped) {
|
|
538
|
+
const tspecResult = await executeTspecRun(additionalTspecFiles, options);
|
|
539
|
+
allResults.push(...tspecResult.data.results);
|
|
540
|
+
parseErrors.push(...tspecResult.data.parseErrors);
|
|
541
|
+
totalTests += tspecResult.data.summary.total;
|
|
542
|
+
totalPassed += tspecResult.data.summary.passed;
|
|
543
|
+
totalFailed += tspecResult.data.summary.failed;
|
|
544
|
+
}
|
|
545
|
+
const summary = {
|
|
546
|
+
total: totalTests,
|
|
547
|
+
passed: totalPassed,
|
|
548
|
+
failed: totalFailed,
|
|
549
|
+
passRate: totalTests > 0 ? totalPassed / totalTests * 100 : 0,
|
|
550
|
+
duration: 0
|
|
551
|
+
};
|
|
552
|
+
let outputStr;
|
|
553
|
+
if (output === "json") {
|
|
554
|
+
outputStr = formatJson({ results: allResults, summary, parseErrors });
|
|
555
|
+
} else {
|
|
556
|
+
const parts = [];
|
|
557
|
+
if (!quiet) {
|
|
558
|
+
parts.push("\n" + chalk.bold("Results:"));
|
|
559
|
+
parts.push(formatTestResults(allResults, summary, { format: output, verbose }));
|
|
560
|
+
} else {
|
|
561
|
+
parts.push(`${summary.passed}/${summary.total} tests passed (${summary.passRate.toFixed(1)}%)`);
|
|
562
|
+
}
|
|
563
|
+
if (parseErrors.length > 0) {
|
|
564
|
+
parts.push(`
|
|
565
|
+
${parseErrors.length} file(s) failed to parse:`);
|
|
566
|
+
parseErrors.forEach(({ file, error: error2 }) => parts.push(` ${file}: ${error2}`));
|
|
567
|
+
}
|
|
568
|
+
outputStr = parts.join("\n");
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
success: totalFailed === 0 && parseErrors.length === 0,
|
|
572
|
+
output: outputStr,
|
|
573
|
+
data: { results: allResults, summary, parseErrors }
|
|
574
|
+
};
|
|
575
|
+
}
|
|
370
576
|
const runCommand = new Command("run").description("Execute test cases and report results").argument("<files...>", "Files or glob patterns to run").option("-o, --output <format>", "Output format: json, text", "text").option("-c, --concurrency <number>", "Max concurrent tests", "5").option("-e, --env <key=value>", "Environment variables", parseKeyValue$1, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue$1, {}).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Only output summary").option("--fail-fast", "Stop on first failure").action(async (files, options) => {
|
|
371
577
|
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
372
578
|
const spinner = options.quiet ? null : ora("Running tests...").start();
|
|
@@ -421,7 +627,7 @@ async function executeParse(params) {
|
|
|
421
627
|
if (fileDescriptors.length === 0) {
|
|
422
628
|
return {
|
|
423
629
|
success: false,
|
|
424
|
-
output: "No .
|
|
630
|
+
output: "No .tcase files found",
|
|
425
631
|
data: {
|
|
426
632
|
testCases: [],
|
|
427
633
|
parseErrors: [],
|
|
@@ -14739,7 +14945,7 @@ const TOOL_DEFINITIONS = [
|
|
|
14739
14945
|
files: {
|
|
14740
14946
|
type: "array",
|
|
14741
14947
|
items: { type: "string" },
|
|
14742
|
-
description: 'Files or glob patterns to run (e.g., ["tests/*.
|
|
14948
|
+
description: 'Files or glob patterns to run (e.g., ["tests/*.tcase"])'
|
|
14743
14949
|
},
|
|
14744
14950
|
concurrency: {
|
|
14745
14951
|
type: "number",
|
|
@@ -14770,7 +14976,7 @@ const TOOL_DEFINITIONS = [
|
|
|
14770
14976
|
},
|
|
14771
14977
|
{
|
|
14772
14978
|
name: "tspec_validate",
|
|
14773
|
-
description: "Validate .
|
|
14979
|
+
description: "Validate .tcase files for schema correctness",
|
|
14774
14980
|
inputSchema: {
|
|
14775
14981
|
type: "object",
|
|
14776
14982
|
properties: {
|
|
@@ -14932,8 +15138,11 @@ const mcpCommand = new Command("mcp").description("Start MCP server for tool int
|
|
|
14932
15138
|
setLoggerOptions({ quiet: true });
|
|
14933
15139
|
await startMcpServer();
|
|
14934
15140
|
});
|
|
15141
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
15142
|
+
const __dirname$1 = dirname(__filename$1);
|
|
15143
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname$1, "../package.json"), "utf-8"));
|
|
14935
15144
|
const program = new Command();
|
|
14936
|
-
program.name("tspec").description("CLI for @boolesai/tspec testing framework").version(
|
|
15145
|
+
program.name("tspec").description("CLI for @boolesai/tspec testing framework").version(packageJson.version);
|
|
14937
15146
|
program.addCommand(validateCommand);
|
|
14938
15147
|
program.addCommand(runCommand);
|
|
14939
15148
|
program.addCommand(parseCommand);
|