qa360 1.4.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/ai.d.ts +41 -0
  3. package/dist/commands/ai.js +499 -0
  4. package/dist/commands/ask.js +12 -12
  5. package/dist/commands/coverage.d.ts +8 -0
  6. package/dist/commands/coverage.js +252 -0
  7. package/dist/commands/explain.d.ts +27 -0
  8. package/dist/commands/explain.js +630 -0
  9. package/dist/commands/flakiness.d.ts +73 -0
  10. package/dist/commands/flakiness.js +435 -0
  11. package/dist/commands/generate.d.ts +66 -0
  12. package/dist/commands/generate.js +438 -0
  13. package/dist/commands/init.d.ts +56 -9
  14. package/dist/commands/init.js +217 -10
  15. package/dist/commands/monitor.d.ts +27 -0
  16. package/dist/commands/monitor.js +225 -0
  17. package/dist/commands/ollama.d.ts +40 -0
  18. package/dist/commands/ollama.js +301 -0
  19. package/dist/commands/pack.d.ts +37 -9
  20. package/dist/commands/pack.js +240 -141
  21. package/dist/commands/regression.d.ts +8 -0
  22. package/dist/commands/regression.js +340 -0
  23. package/dist/commands/repair.d.ts +26 -0
  24. package/dist/commands/repair.js +307 -0
  25. package/dist/commands/retry.d.ts +43 -0
  26. package/dist/commands/retry.js +275 -0
  27. package/dist/commands/run.d.ts +8 -3
  28. package/dist/commands/run.js +45 -31
  29. package/dist/commands/slo.d.ts +8 -0
  30. package/dist/commands/slo.js +327 -0
  31. package/dist/core/adapters/playwright-native-api.d.ts +183 -0
  32. package/dist/core/adapters/playwright-native-api.js +461 -0
  33. package/dist/core/adapters/playwright-ui.d.ts +7 -0
  34. package/dist/core/adapters/playwright-ui.js +29 -1
  35. package/dist/core/ai/anthropic-provider.d.ts +50 -0
  36. package/dist/core/ai/anthropic-provider.js +211 -0
  37. package/dist/core/ai/deepseek-provider.d.ts +81 -0
  38. package/dist/core/ai/deepseek-provider.js +254 -0
  39. package/dist/core/ai/index.d.ts +60 -0
  40. package/dist/core/ai/index.js +18 -0
  41. package/dist/core/ai/llm-client.d.ts +45 -0
  42. package/dist/core/ai/llm-client.js +7 -0
  43. package/dist/core/ai/mock-provider.d.ts +49 -0
  44. package/dist/core/ai/mock-provider.js +121 -0
  45. package/dist/core/ai/ollama-provider.d.ts +78 -0
  46. package/dist/core/ai/ollama-provider.js +192 -0
  47. package/dist/core/ai/openai-provider.d.ts +48 -0
  48. package/dist/core/ai/openai-provider.js +188 -0
  49. package/dist/core/ai/provider-factory.d.ts +160 -0
  50. package/dist/core/ai/provider-factory.js +269 -0
  51. package/dist/core/auth/api-key-provider.d.ts +16 -0
  52. package/dist/core/auth/api-key-provider.js +63 -0
  53. package/dist/core/auth/aws-iam-provider.d.ts +35 -0
  54. package/dist/core/auth/aws-iam-provider.js +177 -0
  55. package/dist/core/auth/azure-ad-provider.d.ts +15 -0
  56. package/dist/core/auth/azure-ad-provider.js +99 -0
  57. package/dist/core/auth/basic-auth-provider.d.ts +26 -0
  58. package/dist/core/auth/basic-auth-provider.js +111 -0
  59. package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
  60. package/dist/core/auth/gcp-adc-provider.js +126 -0
  61. package/dist/core/auth/index.d.ts +238 -0
  62. package/dist/core/auth/index.js +82 -0
  63. package/dist/core/auth/jwt-provider.d.ts +19 -0
  64. package/dist/core/auth/jwt-provider.js +160 -0
  65. package/dist/core/auth/manager.d.ts +84 -0
  66. package/dist/core/auth/manager.js +230 -0
  67. package/dist/core/auth/oauth2-provider.d.ts +17 -0
  68. package/dist/core/auth/oauth2-provider.js +114 -0
  69. package/dist/core/auth/totp-provider.d.ts +31 -0
  70. package/dist/core/auth/totp-provider.js +134 -0
  71. package/dist/core/auth/ui-login-provider.d.ts +26 -0
  72. package/dist/core/auth/ui-login-provider.js +198 -0
  73. package/dist/core/cache/index.d.ts +7 -0
  74. package/dist/core/cache/index.js +6 -0
  75. package/dist/core/cache/lru-cache.d.ts +203 -0
  76. package/dist/core/cache/lru-cache.js +397 -0
  77. package/dist/core/coverage/analyzer.d.ts +101 -0
  78. package/dist/core/coverage/analyzer.js +415 -0
  79. package/dist/core/coverage/collector.d.ts +74 -0
  80. package/dist/core/coverage/collector.js +459 -0
  81. package/dist/core/coverage/config.d.ts +37 -0
  82. package/dist/core/coverage/config.js +156 -0
  83. package/dist/core/coverage/index.d.ts +11 -0
  84. package/dist/core/coverage/index.js +15 -0
  85. package/dist/core/coverage/types.d.ts +267 -0
  86. package/dist/core/coverage/types.js +6 -0
  87. package/dist/core/coverage/vault.d.ts +95 -0
  88. package/dist/core/coverage/vault.js +405 -0
  89. package/dist/core/dashboard/assets.d.ts +6 -0
  90. package/dist/core/dashboard/assets.js +690 -0
  91. package/dist/core/dashboard/index.d.ts +6 -0
  92. package/dist/core/dashboard/index.js +5 -0
  93. package/dist/core/dashboard/server.d.ts +72 -0
  94. package/dist/core/dashboard/server.js +354 -0
  95. package/dist/core/dashboard/types.d.ts +70 -0
  96. package/dist/core/dashboard/types.js +5 -0
  97. package/dist/core/discoverer/index.d.ts +115 -0
  98. package/dist/core/discoverer/index.js +250 -0
  99. package/dist/core/flakiness/index.d.ts +228 -0
  100. package/dist/core/flakiness/index.js +384 -0
  101. package/dist/core/generation/code-formatter.d.ts +111 -0
  102. package/dist/core/generation/code-formatter.js +307 -0
  103. package/dist/core/generation/code-generator.d.ts +144 -0
  104. package/dist/core/generation/code-generator.js +293 -0
  105. package/dist/core/generation/generator.d.ts +40 -0
  106. package/dist/core/generation/generator.js +76 -0
  107. package/dist/core/generation/index.d.ts +30 -0
  108. package/dist/core/generation/index.js +28 -0
  109. package/dist/core/generation/pack-generator.d.ts +107 -0
  110. package/dist/core/generation/pack-generator.js +416 -0
  111. package/dist/core/generation/prompt-builder.d.ts +132 -0
  112. package/dist/core/generation/prompt-builder.js +672 -0
  113. package/dist/core/generation/source-analyzer.d.ts +213 -0
  114. package/dist/core/generation/source-analyzer.js +657 -0
  115. package/dist/core/generation/test-optimizer.d.ts +117 -0
  116. package/dist/core/generation/test-optimizer.js +328 -0
  117. package/dist/core/generation/types.d.ts +214 -0
  118. package/dist/core/generation/types.js +4 -0
  119. package/dist/core/index.d.ts +23 -1
  120. package/dist/core/index.js +39 -0
  121. package/dist/core/pack/validator.js +31 -1
  122. package/dist/core/pack-v2/index.d.ts +9 -0
  123. package/dist/core/pack-v2/index.js +8 -0
  124. package/dist/core/pack-v2/loader.d.ts +62 -0
  125. package/dist/core/pack-v2/loader.js +231 -0
  126. package/dist/core/pack-v2/migrator.d.ts +56 -0
  127. package/dist/core/pack-v2/migrator.js +455 -0
  128. package/dist/core/pack-v2/validator.d.ts +61 -0
  129. package/dist/core/pack-v2/validator.js +577 -0
  130. package/dist/core/regression/detector.d.ts +107 -0
  131. package/dist/core/regression/detector.js +497 -0
  132. package/dist/core/regression/index.d.ts +9 -0
  133. package/dist/core/regression/index.js +11 -0
  134. package/dist/core/regression/trend-analyzer.d.ts +102 -0
  135. package/dist/core/regression/trend-analyzer.js +345 -0
  136. package/dist/core/regression/types.d.ts +222 -0
  137. package/dist/core/regression/types.js +7 -0
  138. package/dist/core/regression/vault.d.ts +87 -0
  139. package/dist/core/regression/vault.js +289 -0
  140. package/dist/core/repair/engine/fixer.d.ts +24 -0
  141. package/dist/core/repair/engine/fixer.js +226 -0
  142. package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
  143. package/dist/core/repair/engine/suggestion-engine.js +187 -0
  144. package/dist/core/repair/index.d.ts +10 -0
  145. package/dist/core/repair/index.js +13 -0
  146. package/dist/core/repair/repairer.d.ts +90 -0
  147. package/dist/core/repair/repairer.js +284 -0
  148. package/dist/core/repair/types.d.ts +91 -0
  149. package/dist/core/repair/types.js +6 -0
  150. package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
  151. package/dist/core/repair/utils/error-analyzer.js +264 -0
  152. package/dist/core/retry/flakiness-integration.d.ts +60 -0
  153. package/dist/core/retry/flakiness-integration.js +228 -0
  154. package/dist/core/retry/index.d.ts +14 -0
  155. package/dist/core/retry/index.js +16 -0
  156. package/dist/core/retry/retry-engine.d.ts +80 -0
  157. package/dist/core/retry/retry-engine.js +296 -0
  158. package/dist/core/retry/types.d.ts +178 -0
  159. package/dist/core/retry/types.js +52 -0
  160. package/dist/core/retry/vault.d.ts +77 -0
  161. package/dist/core/retry/vault.js +304 -0
  162. package/dist/core/runner/e2e-helpers.d.ts +102 -0
  163. package/dist/core/runner/e2e-helpers.js +153 -0
  164. package/dist/core/runner/phase3-runner.d.ts +101 -2
  165. package/dist/core/runner/phase3-runner.js +559 -24
  166. package/dist/core/self-healing/assertion-healer.d.ts +97 -0
  167. package/dist/core/self-healing/assertion-healer.js +371 -0
  168. package/dist/core/self-healing/engine.d.ts +122 -0
  169. package/dist/core/self-healing/engine.js +538 -0
  170. package/dist/core/self-healing/index.d.ts +10 -0
  171. package/dist/core/self-healing/index.js +11 -0
  172. package/dist/core/self-healing/selector-healer.d.ts +103 -0
  173. package/dist/core/self-healing/selector-healer.js +372 -0
  174. package/dist/core/self-healing/types.d.ts +152 -0
  175. package/dist/core/self-healing/types.js +6 -0
  176. package/dist/core/slo/config.d.ts +107 -0
  177. package/dist/core/slo/config.js +360 -0
  178. package/dist/core/slo/index.d.ts +11 -0
  179. package/dist/core/slo/index.js +15 -0
  180. package/dist/core/slo/sli-calculator.d.ts +92 -0
  181. package/dist/core/slo/sli-calculator.js +364 -0
  182. package/dist/core/slo/slo-tracker.d.ts +148 -0
  183. package/dist/core/slo/slo-tracker.js +379 -0
  184. package/dist/core/slo/types.d.ts +281 -0
  185. package/dist/core/slo/types.js +7 -0
  186. package/dist/core/slo/vault.d.ts +102 -0
  187. package/dist/core/slo/vault.js +427 -0
  188. package/dist/core/tui/index.d.ts +7 -0
  189. package/dist/core/tui/index.js +6 -0
  190. package/dist/core/tui/monitor.d.ts +92 -0
  191. package/dist/core/tui/monitor.js +271 -0
  192. package/dist/core/tui/renderer.d.ts +33 -0
  193. package/dist/core/tui/renderer.js +218 -0
  194. package/dist/core/tui/types.d.ts +63 -0
  195. package/dist/core/tui/types.js +5 -0
  196. package/dist/core/types/pack-v2.d.ts +425 -0
  197. package/dist/core/types/pack-v2.js +8 -0
  198. package/dist/core/vault/index.d.ts +116 -0
  199. package/dist/core/vault/index.js +400 -5
  200. package/dist/core/watch/index.d.ts +7 -0
  201. package/dist/core/watch/index.js +6 -0
  202. package/dist/core/watch/watch-mode.d.ts +213 -0
  203. package/dist/core/watch/watch-mode.js +389 -0
  204. package/dist/index.js +68 -68
  205. package/dist/utils/config.d.ts +5 -0
  206. package/dist/utils/config.js +136 -0
  207. package/package.json +5 -1
  208. package/dist/core/adapters/playwright-api.d.ts +0 -82
  209. package/dist/core/adapters/playwright-api.js +0 -264
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Config loader utilities for CLI commands
3
+ */
4
+ import { readFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ export async function loadPackConfig(packPath) {
7
+ const fullPath = join(process.cwd(), packPath);
8
+ const content = readFileSync(fullPath, 'utf8');
9
+ // Try YAML parse
10
+ let config;
11
+ try {
12
+ // Simple YAML parser (basic implementation)
13
+ config = parseYaml(content);
14
+ }
15
+ catch (error) {
16
+ throw new Error(`Failed to parse pack file: ${error}`);
17
+ }
18
+ // Validate version
19
+ if (config.version === 2) {
20
+ return config;
21
+ }
22
+ else if (config.version === 1 || !config.version) {
23
+ return config;
24
+ }
25
+ throw new Error(`Unsupported pack version: ${config.version}`);
26
+ }
27
+ /**
28
+ * Simple YAML parser for pack files
29
+ * This is a basic implementation that handles common cases
30
+ */
31
+ function parseYaml(content) {
32
+ const lines = content.split('\n');
33
+ const result = {};
34
+ const stack = [{ obj: result, indent: -1 }];
35
+ for (let i = 0; i < lines.length; i++) {
36
+ const line = lines[i];
37
+ const trimmed = line.trimRight();
38
+ // Skip empty lines and comments
39
+ if (!trimmed || trimmed.startsWith('#'))
40
+ continue;
41
+ const indent = line.search(/\S|$/);
42
+ // Find parent in stack
43
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
44
+ stack.pop();
45
+ }
46
+ const parent = stack[stack.length - 1].obj;
47
+ const colonIndex = trimmed.indexOf(':');
48
+ if (colonIndex === -1) {
49
+ // List item (array continuation)
50
+ if (Array.isArray(parent)) {
51
+ parent.push(parseValue(trimmed));
52
+ }
53
+ continue;
54
+ }
55
+ const key = trimmed.slice(0, colonIndex).trim();
56
+ const rest = trimmed.slice(colonIndex + 1).trim();
57
+ if (!rest || rest.startsWith('#')) {
58
+ // Object or empty value
59
+ parent[key] = null;
60
+ }
61
+ else if (rest.startsWith('|') || rest.startsWith('>')) {
62
+ // Multi-line string
63
+ const valueLines = [];
64
+ for (let j = i + 1; j < lines.length; j++) {
65
+ const nextLine = lines[j];
66
+ if (nextLine.trim() && !nextLine.startsWith(' '))
67
+ break;
68
+ valueLines.push(nextLine.trim());
69
+ i = j;
70
+ }
71
+ parent[key] = valueLines.join('\n');
72
+ }
73
+ else {
74
+ // Simple value
75
+ parent[key] = parseValue(rest);
76
+ }
77
+ // If next line is more indented, this key starts a nested object/array
78
+ if (i < lines.length - 1) {
79
+ const nextIndent = lines[i + 1].search(/\S|$/);
80
+ if (nextIndent > indent) {
81
+ if (Array.isArray(parent[key])) {
82
+ // Already an array
83
+ }
84
+ else if (typeof parent[key] === 'object' && parent[key] !== null) {
85
+ // Nested object
86
+ stack.push({ obj: parent[key], indent });
87
+ }
88
+ else {
89
+ // Start of array
90
+ const arr = [];
91
+ parent[key] = arr;
92
+ stack.push({ obj: arr, indent });
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ function parseValue(value) {
100
+ value = value.trim();
101
+ // String
102
+ if (value.startsWith('"') || value.startsWith("'")) {
103
+ return value.slice(1, -1);
104
+ }
105
+ // Number
106
+ if (/^-?\d+$/.test(value))
107
+ return parseInt(value, 10);
108
+ if (/^-?\d+\.\d+$/.test(value))
109
+ return parseFloat(value);
110
+ // Boolean
111
+ if (value === 'true')
112
+ return true;
113
+ if (value === 'false')
114
+ return false;
115
+ if (value === 'null' || value === '~')
116
+ return null;
117
+ // Array
118
+ if (value.startsWith('[')) {
119
+ try {
120
+ return JSON.parse(value);
121
+ }
122
+ catch {
123
+ return value;
124
+ }
125
+ }
126
+ // Object
127
+ if (value.startsWith('{')) {
128
+ try {
129
+ return JSON.parse(value);
130
+ }
131
+ catch {
132
+ return value;
133
+ }
134
+ }
135
+ return value;
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "1.4.5",
3
+ "version": "2.0.1",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,10 +37,14 @@
37
37
  "ajv-draft-04": "^1.0.0",
38
38
  "ajv-formats": "^2.1.1",
39
39
  "chalk": "^4.1.2",
40
+ "cli-table3": "^0.6.5",
40
41
  "commander": "^11.0.0",
42
+ "glob": "^10.4.5",
41
43
  "inquirer": "^8.2.7",
42
44
  "js-yaml": "^4.1.0",
45
+ "ollama": "^0.5.1",
43
46
  "ora": "^5.4.1",
47
+ "playwright": "^1.57.0",
44
48
 
45
49
  "sqlite3": "^5.1.6",
46
50
  "tweetnacl": "^1.0.3"
@@ -1,82 +0,0 @@
1
- /**
2
- * QA360 Playwright API Adapter (Socle OOTB)
3
- * Smoke tests for REST/GraphQL APIs with retry logic
4
- */
5
- import { ApiTarget, PackBudgets } from '../types/pack-v1.js';
6
- export interface ApiTestConfig {
7
- target: ApiTarget;
8
- budgets?: PackBudgets;
9
- timeout?: number;
10
- retries?: number;
11
- }
12
- export interface ApiTestResult {
13
- endpoint: string;
14
- method: string;
15
- status: number;
16
- responseTime: number;
17
- success: boolean;
18
- error?: string;
19
- headers?: Record<string, string>;
20
- body?: any;
21
- }
22
- export interface ApiSmokeResult {
23
- success: boolean;
24
- results: ApiTestResult[];
25
- summary: {
26
- total: number;
27
- passed: number;
28
- failed: number;
29
- avgResponseTime: number;
30
- };
31
- junit?: string;
32
- error?: string;
33
- }
34
- export declare class PlaywrightApiAdapter {
35
- private browser?;
36
- private context?;
37
- private redactor;
38
- constructor();
39
- /**
40
- * Execute API smoke tests
41
- */
42
- runSmokeTests(config: ApiTestConfig): Promise<ApiSmokeResult>;
43
- /**
44
- * Execute single API test with retry logic
45
- */
46
- private executeApiTest;
47
- /**
48
- * Parse test specification string
49
- */
50
- private parseTestSpec;
51
- /**
52
- * Check if error is retryable
53
- */
54
- private isRetryableError;
55
- /**
56
- * Calculate test summary
57
- */
58
- private calculateSummary;
59
- /**
60
- * Generate JUnit XML fragment
61
- */
62
- private generateJUnit;
63
- /**
64
- * Escape XML special characters
65
- */
66
- private escapeXml;
67
- /**
68
- * Setup browser context
69
- */
70
- private setupBrowser;
71
- /**
72
- * Cleanup browser resources
73
- */
74
- private cleanup;
75
- /**
76
- * Validate API target configuration
77
- */
78
- static validateConfig(target: ApiTarget): {
79
- valid: boolean;
80
- errors: string[];
81
- };
82
- }
@@ -1,264 +0,0 @@
1
- /**
2
- * QA360 Playwright API Adapter (Socle OOTB)
3
- * Smoke tests for REST/GraphQL APIs with retry logic
4
- */
5
- import { chromium } from '@playwright/test';
6
- import { SecurityRedactor } from '../security/redactor.js';
7
- export class PlaywrightApiAdapter {
8
- browser;
9
- context;
10
- redactor;
11
- constructor() {
12
- this.redactor = SecurityRedactor.forLogs();
13
- }
14
- /**
15
- * Execute API smoke tests
16
- */
17
- async runSmokeTests(config) {
18
- try {
19
- await this.setupBrowser();
20
- const results = [];
21
- const smokeTests = config.target.smoke || [`GET ${config.target.baseUrl}/health -> 200`];
22
- console.log(`🌐 Running API smoke tests (${smokeTests.length} endpoints)`);
23
- for (const test of smokeTests) {
24
- const testResult = await this.executeApiTest(test, config);
25
- results.push(testResult);
26
- if (testResult.success) {
27
- console.log(` ✅ ${testResult.method} ${testResult.endpoint} -> ${testResult.status} (${testResult.responseTime}ms)`);
28
- }
29
- else {
30
- // Show clear error message with actual vs expected
31
- const errorMsg = testResult.error || 'Request failed';
32
- console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${errorMsg}`);
33
- }
34
- }
35
- const summary = this.calculateSummary(results);
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
- }
46
- return {
47
- success: summary.failed === 0,
48
- results,
49
- summary,
50
- junit,
51
- error
52
- };
53
- }
54
- finally {
55
- await this.cleanup();
56
- }
57
- }
58
- /**
59
- * Execute single API test with retry logic
60
- */
61
- async executeApiTest(testSpec, config) {
62
- const { method, endpoint, expectedStatus } = this.parseTestSpec(testSpec, config.target.baseUrl);
63
- const maxRetries = config.retries || 1;
64
- let lastError = '';
65
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
66
- try {
67
- const startTime = Date.now();
68
- const response = await this.context.request.fetch(endpoint, {
69
- method: method,
70
- timeout: config.timeout || 10000,
71
- headers: {
72
- 'User-Agent': 'QA360-API-Smoke/1.0',
73
- 'Accept': 'application/json, text/plain, */*'
74
- }
75
- });
76
- const responseTime = Date.now() - startTime;
77
- const status = response.status();
78
- const success = status === expectedStatus;
79
- // Get response body safely
80
- let body;
81
- try {
82
- const contentType = response.headers()['content-type'] || '';
83
- if (contentType.includes('application/json')) {
84
- body = await response.json();
85
- }
86
- else {
87
- const text = await response.text();
88
- body = text.substring(0, 200); // Limit body size
89
- }
90
- }
91
- catch {
92
- body = '[Response body not readable]';
93
- }
94
- return {
95
- endpoint,
96
- method,
97
- status,
98
- responseTime,
99
- success,
100
- error: success ? undefined : `Expected status ${expectedStatus}, got ${status}`,
101
- headers: response.headers(),
102
- body: this.redactor.redactObject(body)
103
- };
104
- }
105
- catch (error) {
106
- lastError = error instanceof Error ? error.message : 'Unknown error';
107
- // Check if this is a retryable error
108
- const isRetryable = this.isRetryableError(lastError);
109
- if (isRetryable && attempt < maxRetries) {
110
- console.log(` 🔄 Retry ${attempt + 1}/${maxRetries} for ${method} ${endpoint} (${lastError})`);
111
- await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); // Exponential backoff
112
- continue;
113
- }
114
- break;
115
- }
116
- }
117
- return {
118
- endpoint,
119
- method,
120
- status: 0,
121
- responseTime: 0,
122
- success: false,
123
- error: this.redactor.redact(lastError)
124
- };
125
- }
126
- /**
127
- * Parse test specification string
128
- */
129
- parseTestSpec(spec, baseUrl) {
130
- // Format: "GET /path -> 200" or "POST /api/users -> 201"
131
- const match = spec.match(/^(\w+)\s+(.+?)\s*->\s*(\d+)$/);
132
- if (!match) {
133
- throw new Error(`Invalid test spec format: ${spec}. Expected: "METHOD /path -> STATUS"`);
134
- }
135
- const [, method, path, statusStr] = match;
136
- const expectedStatus = parseInt(statusStr, 10);
137
- // Build full endpoint URL
138
- let endpoint = path;
139
- if (!path.startsWith('http')) {
140
- endpoint = baseUrl.replace(/\/$/, '') + (path.startsWith('/') ? path : `/${path}`);
141
- }
142
- return {
143
- method: method.toUpperCase(),
144
- endpoint,
145
- expectedStatus
146
- };
147
- }
148
- /**
149
- * Check if error is retryable
150
- */
151
- isRetryableError(error) {
152
- const retryablePatterns = [
153
- /ECONNRESET/,
154
- /ETIMEDOUT/,
155
- /ECONNREFUSED/,
156
- /502 Bad Gateway/,
157
- /503 Service Unavailable/,
158
- /504 Gateway Timeout/,
159
- /timeout/i,
160
- /network/i
161
- ];
162
- return retryablePatterns.some(pattern => pattern.test(error));
163
- }
164
- /**
165
- * Calculate test summary
166
- */
167
- calculateSummary(results) {
168
- const total = results.length;
169
- const passed = results.filter(r => r.success).length;
170
- const failed = total - passed;
171
- const avgResponseTime = total > 0 ?
172
- Math.round(results.reduce((sum, r) => sum + r.responseTime, 0) / total) : 0;
173
- return { total, passed, failed, avgResponseTime };
174
- }
175
- /**
176
- * Generate JUnit XML fragment
177
- */
178
- generateJUnit(results) {
179
- const summary = this.calculateSummary(results);
180
- const timestamp = new Date().toISOString();
181
- let junit = `<?xml version="1.0" encoding="UTF-8"?>
182
- <testsuite name="API Smoke Tests" tests="${summary.total}" failures="${summary.failed}" time="${summary.avgResponseTime / 1000}" timestamp="${timestamp}">
183
- `;
184
- for (const result of results) {
185
- const testName = `${result.method} ${result.endpoint}`;
186
- const time = result.responseTime / 1000;
187
- junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
188
- `;
189
- if (!result.success) {
190
- junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(result, null, 2))}</failure>
191
- `;
192
- }
193
- junit += ` </testcase>
194
- `;
195
- }
196
- junit += `</testsuite>`;
197
- return junit;
198
- }
199
- /**
200
- * Escape XML special characters
201
- */
202
- escapeXml(str) {
203
- return str
204
- .replace(/&/g, '&amp;')
205
- .replace(/</g, '&lt;')
206
- .replace(/>/g, '&gt;')
207
- .replace(/"/g, '&quot;')
208
- .replace(/'/g, '&apos;');
209
- }
210
- /**
211
- * Setup browser context
212
- */
213
- async setupBrowser() {
214
- this.browser = await chromium.launch({
215
- headless: true,
216
- args: ['--no-sandbox', '--disable-dev-shm-usage']
217
- });
218
- this.context = await this.browser.newContext({
219
- userAgent: 'QA360-API-Smoke/1.0',
220
- extraHTTPHeaders: {
221
- 'Accept': 'application/json, text/plain, */*'
222
- }
223
- });
224
- }
225
- /**
226
- * Cleanup browser resources
227
- */
228
- async cleanup() {
229
- if (this.context) {
230
- await this.context.close();
231
- }
232
- if (this.browser) {
233
- await this.browser.close();
234
- }
235
- }
236
- /**
237
- * Validate API target configuration
238
- */
239
- static validateConfig(target) {
240
- const errors = [];
241
- if (!target.baseUrl) {
242
- errors.push('API target requires baseUrl');
243
- }
244
- else {
245
- try {
246
- new URL(target.baseUrl);
247
- }
248
- catch {
249
- errors.push('API target baseUrl must be a valid URL');
250
- }
251
- }
252
- if (target.smoke) {
253
- for (const test of target.smoke) {
254
- if (!/^\w+\s+.+\s*->\s*\d+$/.test(test)) {
255
- errors.push(`Invalid smoke test format: ${test}. Expected: "METHOD /path -> STATUS"`);
256
- }
257
- }
258
- }
259
- return {
260
- valid: errors.length === 0,
261
- errors
262
- };
263
- }
264
- }