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.
- package/README.md +1 -1
- package/dist/commands/ai.d.ts +41 -0
- package/dist/commands/ai.js +499 -0
- package/dist/commands/ask.js +12 -12
- package/dist/commands/coverage.d.ts +8 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/explain.d.ts +27 -0
- package/dist/commands/explain.js +630 -0
- package/dist/commands/flakiness.d.ts +73 -0
- package/dist/commands/flakiness.js +435 -0
- package/dist/commands/generate.d.ts +66 -0
- package/dist/commands/generate.js +438 -0
- package/dist/commands/init.d.ts +56 -9
- package/dist/commands/init.js +217 -10
- package/dist/commands/monitor.d.ts +27 -0
- package/dist/commands/monitor.js +225 -0
- package/dist/commands/ollama.d.ts +40 -0
- package/dist/commands/ollama.js +301 -0
- package/dist/commands/pack.d.ts +37 -9
- package/dist/commands/pack.js +240 -141
- package/dist/commands/regression.d.ts +8 -0
- package/dist/commands/regression.js +340 -0
- package/dist/commands/repair.d.ts +26 -0
- package/dist/commands/repair.js +307 -0
- package/dist/commands/retry.d.ts +43 -0
- package/dist/commands/retry.js +275 -0
- package/dist/commands/run.d.ts +8 -3
- package/dist/commands/run.js +45 -31
- package/dist/commands/slo.d.ts +8 -0
- package/dist/commands/slo.js +327 -0
- package/dist/core/adapters/playwright-native-api.d.ts +183 -0
- package/dist/core/adapters/playwright-native-api.js +461 -0
- package/dist/core/adapters/playwright-ui.d.ts +7 -0
- package/dist/core/adapters/playwright-ui.js +29 -1
- package/dist/core/ai/anthropic-provider.d.ts +50 -0
- package/dist/core/ai/anthropic-provider.js +211 -0
- package/dist/core/ai/deepseek-provider.d.ts +81 -0
- package/dist/core/ai/deepseek-provider.js +254 -0
- package/dist/core/ai/index.d.ts +60 -0
- package/dist/core/ai/index.js +18 -0
- package/dist/core/ai/llm-client.d.ts +45 -0
- package/dist/core/ai/llm-client.js +7 -0
- package/dist/core/ai/mock-provider.d.ts +49 -0
- package/dist/core/ai/mock-provider.js +121 -0
- package/dist/core/ai/ollama-provider.d.ts +78 -0
- package/dist/core/ai/ollama-provider.js +192 -0
- package/dist/core/ai/openai-provider.d.ts +48 -0
- package/dist/core/ai/openai-provider.js +188 -0
- package/dist/core/ai/provider-factory.d.ts +160 -0
- package/dist/core/ai/provider-factory.js +269 -0
- package/dist/core/auth/api-key-provider.d.ts +16 -0
- package/dist/core/auth/api-key-provider.js +63 -0
- package/dist/core/auth/aws-iam-provider.d.ts +35 -0
- package/dist/core/auth/aws-iam-provider.js +177 -0
- package/dist/core/auth/azure-ad-provider.d.ts +15 -0
- package/dist/core/auth/azure-ad-provider.js +99 -0
- package/dist/core/auth/basic-auth-provider.d.ts +26 -0
- package/dist/core/auth/basic-auth-provider.js +111 -0
- package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
- package/dist/core/auth/gcp-adc-provider.js +126 -0
- package/dist/core/auth/index.d.ts +238 -0
- package/dist/core/auth/index.js +82 -0
- package/dist/core/auth/jwt-provider.d.ts +19 -0
- package/dist/core/auth/jwt-provider.js +160 -0
- package/dist/core/auth/manager.d.ts +84 -0
- package/dist/core/auth/manager.js +230 -0
- package/dist/core/auth/oauth2-provider.d.ts +17 -0
- package/dist/core/auth/oauth2-provider.js +114 -0
- package/dist/core/auth/totp-provider.d.ts +31 -0
- package/dist/core/auth/totp-provider.js +134 -0
- package/dist/core/auth/ui-login-provider.d.ts +26 -0
- package/dist/core/auth/ui-login-provider.js +198 -0
- package/dist/core/cache/index.d.ts +7 -0
- package/dist/core/cache/index.js +6 -0
- package/dist/core/cache/lru-cache.d.ts +203 -0
- package/dist/core/cache/lru-cache.js +397 -0
- package/dist/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/coverage/analyzer.js +415 -0
- package/dist/core/coverage/collector.d.ts +74 -0
- package/dist/core/coverage/collector.js +459 -0
- package/dist/core/coverage/config.d.ts +37 -0
- package/dist/core/coverage/config.js +156 -0
- package/dist/core/coverage/index.d.ts +11 -0
- package/dist/core/coverage/index.js +15 -0
- package/dist/core/coverage/types.d.ts +267 -0
- package/dist/core/coverage/types.js +6 -0
- package/dist/core/coverage/vault.d.ts +95 -0
- package/dist/core/coverage/vault.js +405 -0
- package/dist/core/dashboard/assets.d.ts +6 -0
- package/dist/core/dashboard/assets.js +690 -0
- package/dist/core/dashboard/index.d.ts +6 -0
- package/dist/core/dashboard/index.js +5 -0
- package/dist/core/dashboard/server.d.ts +72 -0
- package/dist/core/dashboard/server.js +354 -0
- package/dist/core/dashboard/types.d.ts +70 -0
- package/dist/core/dashboard/types.js +5 -0
- package/dist/core/discoverer/index.d.ts +115 -0
- package/dist/core/discoverer/index.js +250 -0
- package/dist/core/flakiness/index.d.ts +228 -0
- package/dist/core/flakiness/index.js +384 -0
- package/dist/core/generation/code-formatter.d.ts +111 -0
- package/dist/core/generation/code-formatter.js +307 -0
- package/dist/core/generation/code-generator.d.ts +144 -0
- package/dist/core/generation/code-generator.js +293 -0
- package/dist/core/generation/generator.d.ts +40 -0
- package/dist/core/generation/generator.js +76 -0
- package/dist/core/generation/index.d.ts +30 -0
- package/dist/core/generation/index.js +28 -0
- package/dist/core/generation/pack-generator.d.ts +107 -0
- package/dist/core/generation/pack-generator.js +416 -0
- package/dist/core/generation/prompt-builder.d.ts +132 -0
- package/dist/core/generation/prompt-builder.js +672 -0
- package/dist/core/generation/source-analyzer.d.ts +213 -0
- package/dist/core/generation/source-analyzer.js +657 -0
- package/dist/core/generation/test-optimizer.d.ts +117 -0
- package/dist/core/generation/test-optimizer.js +328 -0
- package/dist/core/generation/types.d.ts +214 -0
- package/dist/core/generation/types.js +4 -0
- package/dist/core/index.d.ts +23 -1
- package/dist/core/index.js +39 -0
- package/dist/core/pack/validator.js +31 -1
- package/dist/core/pack-v2/index.d.ts +9 -0
- package/dist/core/pack-v2/index.js +8 -0
- package/dist/core/pack-v2/loader.d.ts +62 -0
- package/dist/core/pack-v2/loader.js +231 -0
- package/dist/core/pack-v2/migrator.d.ts +56 -0
- package/dist/core/pack-v2/migrator.js +455 -0
- package/dist/core/pack-v2/validator.d.ts +61 -0
- package/dist/core/pack-v2/validator.js +577 -0
- package/dist/core/regression/detector.d.ts +107 -0
- package/dist/core/regression/detector.js +497 -0
- package/dist/core/regression/index.d.ts +9 -0
- package/dist/core/regression/index.js +11 -0
- package/dist/core/regression/trend-analyzer.d.ts +102 -0
- package/dist/core/regression/trend-analyzer.js +345 -0
- package/dist/core/regression/types.d.ts +222 -0
- package/dist/core/regression/types.js +7 -0
- package/dist/core/regression/vault.d.ts +87 -0
- package/dist/core/regression/vault.js +289 -0
- package/dist/core/repair/engine/fixer.d.ts +24 -0
- package/dist/core/repair/engine/fixer.js +226 -0
- package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
- package/dist/core/repair/engine/suggestion-engine.js +187 -0
- package/dist/core/repair/index.d.ts +10 -0
- package/dist/core/repair/index.js +13 -0
- package/dist/core/repair/repairer.d.ts +90 -0
- package/dist/core/repair/repairer.js +284 -0
- package/dist/core/repair/types.d.ts +91 -0
- package/dist/core/repair/types.js +6 -0
- package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
- package/dist/core/repair/utils/error-analyzer.js +264 -0
- package/dist/core/retry/flakiness-integration.d.ts +60 -0
- package/dist/core/retry/flakiness-integration.js +228 -0
- package/dist/core/retry/index.d.ts +14 -0
- package/dist/core/retry/index.js +16 -0
- package/dist/core/retry/retry-engine.d.ts +80 -0
- package/dist/core/retry/retry-engine.js +296 -0
- package/dist/core/retry/types.d.ts +178 -0
- package/dist/core/retry/types.js +52 -0
- package/dist/core/retry/vault.d.ts +77 -0
- package/dist/core/retry/vault.js +304 -0
- package/dist/core/runner/e2e-helpers.d.ts +102 -0
- package/dist/core/runner/e2e-helpers.js +153 -0
- package/dist/core/runner/phase3-runner.d.ts +101 -2
- package/dist/core/runner/phase3-runner.js +559 -24
- package/dist/core/self-healing/assertion-healer.d.ts +97 -0
- package/dist/core/self-healing/assertion-healer.js +371 -0
- package/dist/core/self-healing/engine.d.ts +122 -0
- package/dist/core/self-healing/engine.js +538 -0
- package/dist/core/self-healing/index.d.ts +10 -0
- package/dist/core/self-healing/index.js +11 -0
- package/dist/core/self-healing/selector-healer.d.ts +103 -0
- package/dist/core/self-healing/selector-healer.js +372 -0
- package/dist/core/self-healing/types.d.ts +152 -0
- package/dist/core/self-healing/types.js +6 -0
- package/dist/core/slo/config.d.ts +107 -0
- package/dist/core/slo/config.js +360 -0
- package/dist/core/slo/index.d.ts +11 -0
- package/dist/core/slo/index.js +15 -0
- package/dist/core/slo/sli-calculator.d.ts +92 -0
- package/dist/core/slo/sli-calculator.js +364 -0
- package/dist/core/slo/slo-tracker.d.ts +148 -0
- package/dist/core/slo/slo-tracker.js +379 -0
- package/dist/core/slo/types.d.ts +281 -0
- package/dist/core/slo/types.js +7 -0
- package/dist/core/slo/vault.d.ts +102 -0
- package/dist/core/slo/vault.js +427 -0
- package/dist/core/tui/index.d.ts +7 -0
- package/dist/core/tui/index.js +6 -0
- package/dist/core/tui/monitor.d.ts +92 -0
- package/dist/core/tui/monitor.js +271 -0
- package/dist/core/tui/renderer.d.ts +33 -0
- package/dist/core/tui/renderer.js +218 -0
- package/dist/core/tui/types.d.ts +63 -0
- package/dist/core/tui/types.js +5 -0
- package/dist/core/types/pack-v2.d.ts +425 -0
- package/dist/core/types/pack-v2.js +8 -0
- package/dist/core/vault/index.d.ts +116 -0
- package/dist/core/vault/index.js +400 -5
- package/dist/core/watch/index.d.ts +7 -0
- package/dist/core/watch/index.js +6 -0
- package/dist/core/watch/watch-mode.d.ts +213 -0
- package/dist/core/watch/watch-mode.js +389 -0
- package/dist/index.js +68 -68
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +136 -0
- package/package.json +5 -1
- package/dist/core/adapters/playwright-api.d.ts +0 -82
- 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": "
|
|
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, '&')
|
|
205
|
-
.replace(/</g, '<')
|
|
206
|
-
.replace(/>/g, '>')
|
|
207
|
-
.replace(/"/g, '"')
|
|
208
|
-
.replace(/'/g, ''');
|
|
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
|
-
}
|