qa360 1.3.0 → 1.3.2

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
@@ -37,12 +37,15 @@ npx qa360@latest doctor
37
37
  ### 1. Generate a test pack
38
38
 
39
39
  ```bash
40
- # Interactive mode (recommended for first-time users)
40
+ # Option 1: Interactive mode (recommended for first-time users)
41
41
  qa360 init
42
42
 
43
- # Or use a template directly
43
+ # Option 2: Use a template directly
44
44
  qa360 init --template api-basic --yes
45
- qa360 init --template fullstack --yes
45
+
46
+ # Option 3: Copy an example
47
+ qa360 examples copy api-basic
48
+ qa360 examples list # See all available examples
46
49
  ```
47
50
 
48
51
  ### 2. Run tests
@@ -69,6 +72,11 @@ qa360 init --template fullstack
69
72
  qa360 init --template security
70
73
  qa360 init --template complete
71
74
 
75
+ # Examples commands
76
+ qa360 examples list # List all examples
77
+ qa360 examples copy api-basic # Copy example to qa360.yml
78
+ qa360 examples show fullstack # Preview example content
79
+
72
80
  # System health check
73
81
  qa360 doctor
74
82
 
@@ -120,6 +128,7 @@ qa360 run examples/api-basic.yml
120
128
  | Command | Description |
121
129
  |---------|-------------|
122
130
  | `init` | Generate test pack interactively |
131
+ | `examples` | Manage example templates (list, copy, show) |
123
132
  | `run` | Execute test pack |
124
133
  | `verify` | Verify cryptographic proof bundles |
125
134
  | `doctor` | Check system health, auto-fix issues |
@@ -0,0 +1,34 @@
1
+ /**
2
+ * QA360 Examples Command - Manage example templates
3
+ *
4
+ * Usage:
5
+ * qa360 examples list
6
+ * qa360 examples copy api-basic
7
+ * qa360 examples show fullstack
8
+ */
9
+ /**
10
+ * CLI options for examples command
11
+ */
12
+ export interface ExamplesOptions {
13
+ output?: string;
14
+ force?: boolean;
15
+ }
16
+ /**
17
+ * List available examples
18
+ */
19
+ export declare function examplesListCommand(): Promise<void>;
20
+ /**
21
+ * Show example content
22
+ */
23
+ export declare function examplesShowCommand(templateName: string): Promise<void>;
24
+ /**
25
+ * Copy example to current directory
26
+ */
27
+ export declare function examplesCopyCommand(templateName: string, options?: ExamplesOptions): Promise<void>;
28
+ declare const _default: {
29
+ list: typeof examplesListCommand;
30
+ show: typeof examplesShowCommand;
31
+ copy: typeof examplesCopyCommand;
32
+ };
33
+ export default _default;
34
+ //# sourceMappingURL=examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"examples.d.ts","sourceRoot":"","sources":["../../src/commands/examples.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA8DD;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkCzD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwC7E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA6Df;;;;;;AAED,wBAIE"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * QA360 Examples Command - Manage example templates
3
+ *
4
+ * Usage:
5
+ * qa360 examples list
6
+ * qa360 examples copy api-basic
7
+ * qa360 examples show fullstack
8
+ */
9
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import chalk from 'chalk';
13
+ import inquirer from 'inquirer';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ /**
17
+ * Get path to examples directory
18
+ */
19
+ function getExamplesDir() {
20
+ // In development: /Users/.../QA360/cli/src/commands -> ../../examples
21
+ // In production: /usr/local/lib/node_modules/qa360/dist/commands -> ../../examples
22
+ const examplesDir = join(__dirname, '../../examples');
23
+ if (!existsSync(examplesDir)) {
24
+ throw new Error(`Examples directory not found at: ${examplesDir}`);
25
+ }
26
+ return examplesDir;
27
+ }
28
+ /**
29
+ * Parse example file for metadata
30
+ */
31
+ function parseExampleMetadata(filePath) {
32
+ try {
33
+ const content = readFileSync(filePath, 'utf-8');
34
+ const lines = content.split('\n');
35
+ // Extract description from first comment line
36
+ const descLine = lines.find(l => l.startsWith('# QA360 Example:'));
37
+ const description = descLine
38
+ ? descLine.replace('# QA360 Example:', '').trim()
39
+ : 'No description';
40
+ // Extract gates from content
41
+ const gatesMatch = content.match(/gates:\s*\n((?:\s*-\s*.+\n)*)/);
42
+ const gates = [];
43
+ if (gatesMatch) {
44
+ const gatesBlock = gatesMatch[1];
45
+ const gateLines = gatesBlock.match(/- (.+)/g);
46
+ if (gateLines) {
47
+ gates.push(...gateLines.map(line => line.replace('- ', '').trim()));
48
+ }
49
+ }
50
+ return {
51
+ name: filePath.split('/').pop()?.replace('.yml', '') || '',
52
+ description,
53
+ gates,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * List available examples
62
+ */
63
+ export async function examplesListCommand() {
64
+ try {
65
+ console.log(chalk.bold.cyan('\n📚 Available QA360 Examples\n'));
66
+ const examplesDir = getExamplesDir();
67
+ const files = readdirSync(examplesDir)
68
+ .filter(f => f.endsWith('.yml') && f !== 'README.md')
69
+ .sort();
70
+ if (files.length === 0) {
71
+ console.log(chalk.yellow('No examples found.'));
72
+ return;
73
+ }
74
+ files.forEach(file => {
75
+ const filePath = join(examplesDir, file);
76
+ const metadata = parseExampleMetadata(filePath);
77
+ if (metadata) {
78
+ const templateName = file.replace('.yml', '');
79
+ console.log(chalk.bold.green(`📄 ${templateName}`));
80
+ console.log(chalk.gray(` ${metadata.description}`));
81
+ console.log(chalk.blue(` Gates: ${metadata.gates.join(', ')}`));
82
+ console.log(chalk.gray(` Copy: qa360 examples copy ${templateName}\n`));
83
+ }
84
+ });
85
+ console.log(chalk.cyan('💡 Tip: Use "qa360 examples copy <name>" to copy an example\n'));
86
+ }
87
+ catch (error) {
88
+ console.error(chalk.red('\n❌ Failed to list examples:'));
89
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
90
+ process.exit(1);
91
+ }
92
+ }
93
+ /**
94
+ * Show example content
95
+ */
96
+ export async function examplesShowCommand(templateName) {
97
+ try {
98
+ if (!templateName) {
99
+ console.error(chalk.red('\n❌ Example name required'));
100
+ console.log(chalk.gray(' Usage: qa360 examples show <name>'));
101
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
102
+ process.exit(1);
103
+ }
104
+ const examplesDir = getExamplesDir();
105
+ const fileName = templateName.endsWith('.yml') ? templateName : `${templateName}.yml`;
106
+ const filePath = join(examplesDir, fileName);
107
+ if (!existsSync(filePath)) {
108
+ console.error(chalk.red(`\n❌ Example not found: ${templateName}`));
109
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
110
+ process.exit(1);
111
+ }
112
+ const content = readFileSync(filePath, 'utf-8');
113
+ const metadata = parseExampleMetadata(filePath);
114
+ console.log(chalk.bold.cyan(`\n📄 Example: ${templateName}\n`));
115
+ if (metadata) {
116
+ console.log(chalk.gray(`Description: ${metadata.description}`));
117
+ console.log(chalk.gray(`Gates: ${metadata.gates.join(', ')}\n`));
118
+ }
119
+ console.log(chalk.dim('─'.repeat(60)));
120
+ console.log(content);
121
+ console.log(chalk.dim('─'.repeat(60)));
122
+ console.log(chalk.cyan(`\n💡 Copy this example: qa360 examples copy ${templateName}\n`));
123
+ }
124
+ catch (error) {
125
+ console.error(chalk.red('\n❌ Failed to show example:'));
126
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
127
+ process.exit(1);
128
+ }
129
+ }
130
+ /**
131
+ * Copy example to current directory
132
+ */
133
+ export async function examplesCopyCommand(templateName, options = {}) {
134
+ try {
135
+ if (!templateName) {
136
+ console.error(chalk.red('\n❌ Example name required'));
137
+ console.log(chalk.gray(' Usage: qa360 examples copy <name>'));
138
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
139
+ process.exit(1);
140
+ }
141
+ const examplesDir = getExamplesDir();
142
+ const fileName = templateName.endsWith('.yml') ? templateName : `${templateName}.yml`;
143
+ const sourcePath = join(examplesDir, fileName);
144
+ if (!existsSync(sourcePath)) {
145
+ console.error(chalk.red(`\n❌ Example not found: ${templateName}`));
146
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
147
+ process.exit(1);
148
+ }
149
+ // Determine output path
150
+ const outputFile = options.output || join(process.cwd(), 'qa360.yml');
151
+ // Check if file exists
152
+ if (existsSync(outputFile) && !options.force) {
153
+ const overwrite = await inquirer.prompt([
154
+ {
155
+ type: 'confirm',
156
+ name: 'overwrite',
157
+ message: `File ${outputFile} already exists. Overwrite?`,
158
+ default: false,
159
+ },
160
+ ]);
161
+ if (!overwrite.overwrite) {
162
+ console.log(chalk.yellow('\n⚠️ Aborted. Use --force to overwrite.\n'));
163
+ return;
164
+ }
165
+ }
166
+ // Copy file
167
+ const content = readFileSync(sourcePath, 'utf-8');
168
+ writeFileSync(outputFile, content, 'utf-8');
169
+ console.log(chalk.green.bold('\n✅ Example copied successfully!'));
170
+ console.log(chalk.gray(`\n📄 File: ${outputFile}`));
171
+ const metadata = parseExampleMetadata(sourcePath);
172
+ if (metadata) {
173
+ console.log(chalk.gray(`📋 Gates: ${metadata.gates.join(', ')}`));
174
+ }
175
+ console.log(chalk.cyan('\n📚 Next steps:'));
176
+ console.log(chalk.gray(' 1. Edit qa360.yml to customize for your project'));
177
+ console.log(chalk.gray(' 2. Run: qa360 run'));
178
+ console.log(chalk.gray(' 3. Verify: qa360 verify .qa360/runs/\n'));
179
+ }
180
+ catch (error) {
181
+ console.error(chalk.red('\n❌ Failed to copy example:'));
182
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
183
+ process.exit(1);
184
+ }
185
+ }
186
+ export default {
187
+ list: examplesListCommand,
188
+ show: examplesShowCommand,
189
+ copy: examplesCopyCommand,
190
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAgB,KAAK,eAAe,EAAkC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEzH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAwBtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CA8B5D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAGD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAgB,KAAK,eAAe,EAAkC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEzH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CA+B5D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAGD,eAAe,UAAU,CAAC"}
@@ -24,7 +24,23 @@ export async function loadPack(packPath) {
24
24
  const validator = new PackValidator();
25
25
  const validation = await validator.validate(pack);
26
26
  if (!validation.valid) {
27
- const errors = validation.errors?.map(e => ` • ${e}`).join('\n');
27
+ const errors = validation.errors?.map(e => {
28
+ // Handle both string and object errors
29
+ if (typeof e === 'string') {
30
+ return ` • ${e}`;
31
+ }
32
+ else if (e && typeof e === 'object') {
33
+ // Extract meaningful info from error objects
34
+ const errObj = e;
35
+ if (errObj.message)
36
+ return ` • ${errObj.message}`;
37
+ if (errObj.instancePath && errObj.message) {
38
+ return ` • ${errObj.instancePath}: ${errObj.message}`;
39
+ }
40
+ return ` • ${JSON.stringify(e)}`;
41
+ }
42
+ return ` • ${String(e)}`;
43
+ }).join('\n') || ' • Unknown validation error';
28
44
  throw new Error(`Invalid pack configuration:\n${errors}`);
29
45
  }
30
46
  // Show warnings if any
@@ -83,7 +99,8 @@ export function displayResults(result) {
83
99
  if (failed.length > 0) {
84
100
  console.log(chalk.red('\nFailed gates:'));
85
101
  failed.forEach((g) => {
86
- console.log(chalk.red(` • ${g.gate}: ${g.error || 'Unknown error'}`));
102
+ const errorMessage = g.error || 'Test failed - check logs above for details';
103
+ console.log(chalk.red(` • ${g.gate}: ${errorMessage}`));
87
104
  });
88
105
  }
89
106
  }
@@ -29,6 +29,7 @@ export interface ApiSmokeResult {
29
29
  avgResponseTime: number;
30
30
  };
31
31
  junit?: string;
32
+ error?: string;
32
33
  }
33
34
  export declare class PlaywrightApiAdapter {
34
35
  private browser?;
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-api.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/playwright-api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAmB;;IAMnC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAmCnE;;OAEG;YACW,cAAc;IA0E5B;;OAEG;IACH,OAAO,CAAC,aAAa;IA4BrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,aAAa;IA6BrB;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;YACW,YAAY;IAc1B;;OAEG;YACW,OAAO;IASrB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;CA0B/E"}
1
+ {"version":3,"file":"playwright-api.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/playwright-api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAmB;;IAMnC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAgDnE;;OAEG;YACW,cAAc;IA0E5B;;OAEG;IACH,OAAO,CAAC,aAAa;IA4BrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,aAAa;IA6BrB;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;YACW,YAAY;IAc1B;;OAEG;YACW,OAAO;IASrB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;CA0B/E"}
@@ -27,16 +27,28 @@ export class PlaywrightApiAdapter {
27
27
  console.log(` ✅ ${testResult.method} ${testResult.endpoint} -> ${testResult.status} (${testResult.responseTime}ms)`);
28
28
  }
29
29
  else {
30
- console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${testResult.error}`);
30
+ // Show clear error message with actual vs expected
31
+ const errorMsg = testResult.error || 'Request failed';
32
+ console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${errorMsg}`);
31
33
  }
32
34
  }
33
35
  const summary = this.calculateSummary(results);
34
36
  const junit = this.generateJUnit(results);
37
+ // Generate error message from failed tests
38
+ let error;
39
+ if (summary.failed > 0) {
40
+ const failedTests = results.filter(r => !r.success);
41
+ const errorMessages = failedTests.map(t => t.error).filter(Boolean);
42
+ error = errorMessages.length > 0
43
+ ? `${summary.failed} endpoint(s) failed: ${errorMessages[0]}${errorMessages.length > 1 ? ` (and ${errorMessages.length - 1} more)` : ''}`
44
+ : `${summary.failed} endpoint(s) failed`;
45
+ }
35
46
  return {
36
47
  success: summary.failed === 0,
37
48
  results,
38
49
  summary,
39
- junit
50
+ junit,
51
+ error
40
52
  };
41
53
  }
42
54
  finally {
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ const version = packageJson.version;
17
17
  import { doctorCommand } from './commands/doctor.js';
18
18
  import { askCommand } from './commands/ask.js';
19
19
  import { initCommand } from './commands/init.js';
20
+ import { examplesListCommand, examplesCopyCommand, examplesShowCommand } from './commands/examples.js';
20
21
  import { runCommand } from './commands/run.js';
21
22
  import { reportCommand } from './commands/report.js';
22
23
  import { verifyCommand } from './commands/verify.js';
@@ -48,6 +49,30 @@ program
48
49
  .action(async (options) => {
49
50
  await initCommand(options);
50
51
  });
52
+ // Examples commands
53
+ const examplesCommand = program
54
+ .command('examples')
55
+ .description('Manage example templates');
56
+ examplesCommand
57
+ .command('list')
58
+ .description('List all available example templates')
59
+ .action(async () => {
60
+ await examplesListCommand();
61
+ });
62
+ examplesCommand
63
+ .command('copy <template>')
64
+ .description('Copy example template to current directory')
65
+ .option('--output <file>', 'Output file path', 'qa360.yml')
66
+ .option('--force', 'Overwrite existing file')
67
+ .action(async (template, options) => {
68
+ await examplesCopyCommand(template, options);
69
+ });
70
+ examplesCommand
71
+ .command('show <template>')
72
+ .description('Show example template content')
73
+ .action(async (template) => {
74
+ await examplesShowCommand(template);
75
+ });
51
76
  program
52
77
  .command('ask [query]')
53
78
  .description('Natural language test requests - generates pack.yml')
@@ -42,15 +42,17 @@ security:
42
42
  dast:
43
43
  max_high: 5
44
44
 
45
- # Docker Compose integration
46
- hooks:
47
- beforeAll:
48
- - compose: up
49
- timeout: 30000
50
- - wait_on: http://localhost:3000
51
-
52
- afterAll:
53
- - compose: down
45
+ # Docker Compose integration (optional - remove if not using Docker)
46
+ # hooks:
47
+ # beforeAll:
48
+ # - run: "docker compose up -d"
49
+ # timeout: 30000
50
+ # - run: "npx wait-on http://localhost:3000"
51
+ # timeout: 30000
52
+ #
53
+ # afterAll:
54
+ # - run: "docker compose down"
55
+ # timeout: 30000
54
56
 
55
57
  # Execution settings
56
58
  execution:
@@ -26,15 +26,17 @@ targets:
26
26
  budgets:
27
27
  perf_p95_ms: 2000 # P95 latency must be < 2000ms
28
28
 
29
- # Hooks for local development
30
- hooks:
31
- beforeAll:
32
- - compose: up
33
- timeout: 30000
34
- - wait_on: http://localhost:3000
35
-
36
- afterAll:
37
- - compose: down
29
+ # Hooks for local development (optional - remove if not using Docker)
30
+ # hooks:
31
+ # beforeAll:
32
+ # - run: "docker compose up -d"
33
+ # timeout: 30000
34
+ # - run: "npx wait-on http://localhost:3000"
35
+ # timeout: 30000
36
+ #
37
+ # afterAll:
38
+ # - run: "docker compose down"
39
+ # timeout: 30000
38
40
 
39
41
  execution:
40
42
  timeout: 60000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "prepublishOnly": "npm run build:bundle && sed -i.bak 's/\"qa360-core\": \"workspace:\\*\",//g' package.json && rm -f package.json.bak",
28
28
  "postpublish": "git checkout package.json 2>/dev/null || true",
29
29
  "publish:dry": "npm run build:bundle && sed -i.bak 's/\"qa360-core\": \"workspace:\\*\",//g' package.json && npm publish --dry-run; git checkout package.json",
30
- "publish:real": "npm run build:bundle && npm publish --access public"
30
+ "publish:real": "npm run build:bundle && npm publish --access public --provenance"
31
31
  },
32
32
  "dependencies": {
33
33
  "@playwright/test": "^1.49.0",