@boolesai/tspec-cli 1.1.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 CHANGED
@@ -21,7 +21,7 @@ npx @boolesai/tspec-cli <command>
21
21
 
22
22
  ### `tspec validate`
23
23
 
24
- Validate `.tspec` files for schema correctness.
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.tspec
37
+ tspec validate tests/login.http.tcase
38
38
 
39
39
  # Validate multiple files with glob pattern
40
- tspec validate "tests/**/*.tspec"
40
+ tspec validate "tests/**/*.tcase"
41
41
 
42
42
  # JSON output for CI/CD
43
- tspec validate tests/*.tspec --output json
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.tspec
66
+ tspec run tests/*.http.tcase
67
67
 
68
68
  # Run with environment variables
69
- tspec run tests/*.tspec -e API_HOST=api.example.com -e API_KEY=secret
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/*.tspec -p username=testuser -p timeout=5000
72
+ tspec run tests/*.tcase -p username=testuser -p timeout=5000
73
73
 
74
74
  # Run with higher concurrency
75
- tspec run tests/*.tspec -c 10
75
+ tspec run tests/*.tcase -c 10
76
76
 
77
77
  # Verbose output for debugging
78
- tspec run tests/*.tspec -v
78
+ tspec run tests/*.tcase -v
79
79
 
80
80
  # JSON output for CI/CD
81
- tspec run tests/*.tspec --output json
81
+ tspec run tests/*.tcase --output json
82
82
 
83
83
  # Stop on first failure
84
- tspec run tests/*.tspec --fail-fast
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.tspec
105
+ tspec parse tests/login.http.tcase
106
106
 
107
107
  # JSON output for inspection
108
- tspec parse tests/*.tspec --output json
108
+ tspec parse tests/*.tcase --output json
109
109
 
110
110
  # With variable substitution
111
- tspec parse tests/*.tspec -e API_HOST=localhost
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 .tspec files for schema correctness |
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/*.tspec"],
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/*.tspec"],
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/*.tspec"],
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/*.tspec --output json > results.json
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/*.tspec
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/*.tspec --output json
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(".tspec")) {
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("**/*.tspec", { cwd: absolutePath, absolute: true });
24
+ const dirFiles = await glob("**/*.tcase", { cwd: absolutePath, absolute: true });
24
25
  if (dirFiles.length === 0) {
25
- errors2.push(`No .tspec files found in directory: ${pattern2}`);
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(".tspec"));
34
+ const tspecMatches = matches.filter((f) => f.endsWith(".tcase"));
34
35
  if (tspecMatches.length === 0) {
35
- errors2.push(`No .tspec files matched pattern: ${pattern2}`);
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
  }
@@ -199,7 +255,7 @@ async function executeValidate(params) {
199
255
  if (fileDescriptors.length === 0) {
200
256
  return {
201
257
  success: false,
202
- output: "No .tspec files found",
258
+ output: "No .tcase files found",
203
259
  data: { results: [] }
204
260
  };
205
261
  }
@@ -222,7 +278,7 @@ async function executeValidate(params) {
222
278
  data: { results }
223
279
  };
224
280
  }
225
- const validateCommand = new Command("validate").description("Validate .tspec 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) => {
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) => {
226
282
  setLoggerOptions({ quiet: options.quiet });
227
283
  const spinner = options.quiet ? null : ora("Validating...").start();
228
284
  try {
@@ -304,11 +360,11 @@ async function executeRun(params) {
304
360
  const output = params.output ?? "text";
305
361
  const verbose = params.verbose ?? false;
306
362
  const quiet = params.quiet ?? false;
307
- const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(params.files);
308
- if (fileDescriptors.length === 0) {
363
+ const { tspecFiles, suiteFiles, errors: resolveErrors } = await discoverAllTestFiles(params.files);
364
+ if (tspecFiles.length === 0 && suiteFiles.length === 0) {
309
365
  return {
310
366
  success: false,
311
- output: "No .tspec files found",
367
+ output: "No .tcase or .tsuite files found",
312
368
  data: {
313
369
  results: [],
314
370
  summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
@@ -316,6 +372,29 @@ async function executeRun(params) {
316
372
  }
317
373
  };
318
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;
319
398
  const allResults = [];
320
399
  const parseErrors = [];
321
400
  let totalTestCases = 0;
@@ -384,6 +463,116 @@ ${parseErrors.length} file(s) failed to parse:`);
384
463
  data: { results: formattedResults, summary, parseErrors }
385
464
  };
386
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
+ }
387
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) => {
388
577
  setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
389
578
  const spinner = options.quiet ? null : ora("Running tests...").start();
@@ -438,7 +627,7 @@ async function executeParse(params) {
438
627
  if (fileDescriptors.length === 0) {
439
628
  return {
440
629
  success: false,
441
- output: "No .tspec files found",
630
+ output: "No .tcase files found",
442
631
  data: {
443
632
  testCases: [],
444
633
  parseErrors: [],
@@ -14756,7 +14945,7 @@ const TOOL_DEFINITIONS = [
14756
14945
  files: {
14757
14946
  type: "array",
14758
14947
  items: { type: "string" },
14759
- description: 'Files or glob patterns to run (e.g., ["tests/*.tspec"])'
14948
+ description: 'Files or glob patterns to run (e.g., ["tests/*.tcase"])'
14760
14949
  },
14761
14950
  concurrency: {
14762
14951
  type: "number",
@@ -14787,7 +14976,7 @@ const TOOL_DEFINITIONS = [
14787
14976
  },
14788
14977
  {
14789
14978
  name: "tspec_validate",
14790
- description: "Validate .tspec files for schema correctness",
14979
+ description: "Validate .tcase files for schema correctness",
14791
14980
  inputSchema: {
14792
14981
  type: "object",
14793
14982
  properties: {
@@ -14949,8 +15138,11 @@ const mcpCommand = new Command("mcp").description("Start MCP server for tool int
14949
15138
  setLoggerOptions({ quiet: true });
14950
15139
  await startMcpServer();
14951
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"));
14952
15144
  const program = new Command();
14953
- program.name("tspec").description("CLI for @boolesai/tspec testing framework").version("1.0.0");
15145
+ program.name("tspec").description("CLI for @boolesai/tspec testing framework").version(packageJson.version);
14954
15146
  program.addCommand(validateCommand);
14955
15147
  program.addCommand(runCommand);
14956
15148
  program.addCommand(parseCommand);