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,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Pack v2 Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates pack.yml v2 files with test_files patterns and auth profiles.
|
|
5
|
+
*/
|
|
6
|
+
import { glob } from 'glob';
|
|
7
|
+
import { resolve, dirname } from 'path';
|
|
8
|
+
export class PackValidatorV2 {
|
|
9
|
+
packPath;
|
|
10
|
+
baseDir;
|
|
11
|
+
constructor(packPath) {
|
|
12
|
+
this.packPath = packPath;
|
|
13
|
+
this.baseDir = dirname(resolve(packPath));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate a Pack v2 configuration
|
|
17
|
+
*/
|
|
18
|
+
async validate(pack, options) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const info = {
|
|
22
|
+
gatesCount: 0,
|
|
23
|
+
totalTests: 0,
|
|
24
|
+
frameworks: []
|
|
25
|
+
};
|
|
26
|
+
// 1. Validate structure
|
|
27
|
+
const structureErrors = this.validateStructure(pack);
|
|
28
|
+
errors.push(...structureErrors);
|
|
29
|
+
if (errors.length > 0) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
version: 2,
|
|
33
|
+
errors,
|
|
34
|
+
warnings,
|
|
35
|
+
info
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// 2. Validate version
|
|
39
|
+
if (pack.version !== 2) {
|
|
40
|
+
errors.push({
|
|
41
|
+
code: 'QP2V001',
|
|
42
|
+
message: `Invalid version: expected 2, got ${pack.version}`,
|
|
43
|
+
path: 'version'
|
|
44
|
+
});
|
|
45
|
+
return { valid: false, version: 2, errors, warnings, info };
|
|
46
|
+
}
|
|
47
|
+
// 3. Validate required fields
|
|
48
|
+
const requiredErrors = this.validateRequiredFields(pack);
|
|
49
|
+
errors.push(...requiredErrors);
|
|
50
|
+
// 4. Validate auth configuration
|
|
51
|
+
const authErrors = await this.validateAuthConfig(pack.auth);
|
|
52
|
+
errors.push(...authErrors.errors);
|
|
53
|
+
warnings.push(...authErrors.warnings);
|
|
54
|
+
// 5. Validate gates
|
|
55
|
+
const gatesResult = await this.validateGates(pack.gates, options?.checkFilesExist ?? false);
|
|
56
|
+
errors.push(...gatesResult.errors);
|
|
57
|
+
warnings.push(...gatesResult.warnings);
|
|
58
|
+
info.gatesCount = Object.keys(pack.gates || {}).length;
|
|
59
|
+
info.totalTests = gatesResult.totalTests;
|
|
60
|
+
info.frameworks = gatesResult.frameworks;
|
|
61
|
+
// 6. Validate hooks
|
|
62
|
+
const hooksResult = this.validateHooks(pack.hooks);
|
|
63
|
+
errors.push(...hooksResult.errors);
|
|
64
|
+
warnings.push(...hooksResult.warnings);
|
|
65
|
+
// 7. Validate execution config
|
|
66
|
+
const execErrors = this.validateExecutionConfig(pack.execution);
|
|
67
|
+
errors.push(...execErrors.errors);
|
|
68
|
+
warnings.push(...execErrors.warnings);
|
|
69
|
+
// 8. Business logic validation
|
|
70
|
+
const businessErrors = this.validateBusinessRules(pack);
|
|
71
|
+
errors.push(...businessErrors.errors);
|
|
72
|
+
warnings.push(...businessErrors.warnings);
|
|
73
|
+
return {
|
|
74
|
+
valid: errors.length === 0,
|
|
75
|
+
version: 2,
|
|
76
|
+
errors,
|
|
77
|
+
warnings,
|
|
78
|
+
info
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate basic structure
|
|
83
|
+
*/
|
|
84
|
+
validateStructure(pack) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
if (!pack || typeof pack !== 'object') {
|
|
87
|
+
errors.push({
|
|
88
|
+
code: 'QP2V002',
|
|
89
|
+
message: 'Pack configuration must be an object',
|
|
90
|
+
path: 'root'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate required fields
|
|
97
|
+
*/
|
|
98
|
+
validateRequiredFields(pack) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
if (!pack.name || typeof pack.name !== 'string') {
|
|
101
|
+
errors.push({
|
|
102
|
+
code: 'QP2V003',
|
|
103
|
+
message: 'Pack name is required and must be a string',
|
|
104
|
+
path: 'name',
|
|
105
|
+
suggestion: 'Add: name: "my-pack"'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!pack.gates || typeof pack.gates !== 'object') {
|
|
109
|
+
errors.push({
|
|
110
|
+
code: 'QP2V004',
|
|
111
|
+
message: 'Gates configuration is required and must be an object',
|
|
112
|
+
path: 'gates',
|
|
113
|
+
suggestion: 'Add: gates: { smoke: { adapter: "playwright-api", test_files: ["tests/**/*.spec.ts"] } }'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (pack.gates && Object.keys(pack.gates).length === 0) {
|
|
117
|
+
errors.push({
|
|
118
|
+
code: 'QP2V005',
|
|
119
|
+
message: 'At least one gate must be defined',
|
|
120
|
+
path: 'gates',
|
|
121
|
+
suggestion: 'Add at least one gate configuration'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return errors;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Validate auth configuration
|
|
128
|
+
*/
|
|
129
|
+
async validateAuthConfig(auth) {
|
|
130
|
+
const errors = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
if (!auth) {
|
|
133
|
+
return { errors, warnings };
|
|
134
|
+
}
|
|
135
|
+
// Validate default auth profiles reference existing profiles
|
|
136
|
+
const profiles = auth.profiles || {};
|
|
137
|
+
const validAuthTypes = ['none', 'jwt', 'oauth2', 'api_key', 'bearer', 'basic', 'totp', 'ui_login', 'gcp_adc', 'aws_iam', 'azure_ad'];
|
|
138
|
+
if (auth.api && !profiles[auth.api]) {
|
|
139
|
+
errors.push({
|
|
140
|
+
code: 'QP2V006',
|
|
141
|
+
message: `Auth profile "${auth.api}" referenced in auth.api but not defined in profiles`,
|
|
142
|
+
path: 'auth.api',
|
|
143
|
+
suggestion: `Define the profile or use an existing one: ${Object.keys(profiles).join(', ') || 'none defined'}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (auth.ui && !profiles[auth.ui]) {
|
|
147
|
+
errors.push({
|
|
148
|
+
code: 'QP2V007',
|
|
149
|
+
message: `Auth profile "${auth.ui}" referenced in auth.ui but not defined in profiles`,
|
|
150
|
+
path: 'auth.ui',
|
|
151
|
+
suggestion: `Define the profile or use an existing one: ${Object.keys(profiles).join(', ') || 'none defined'}`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Validate each profile
|
|
155
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
156
|
+
const profileErrors = this.validateAuthProfile(name, profile, validAuthTypes);
|
|
157
|
+
errors.push(...profileErrors.errors);
|
|
158
|
+
warnings.push(...profileErrors.warnings);
|
|
159
|
+
}
|
|
160
|
+
return { errors, warnings };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate a single auth profile
|
|
164
|
+
*/
|
|
165
|
+
validateAuthProfile(name, profile, validTypes) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
const warnings = [];
|
|
168
|
+
if (!validTypes.includes(profile.type)) {
|
|
169
|
+
errors.push({
|
|
170
|
+
code: 'QP2V008',
|
|
171
|
+
message: `Invalid auth type: ${profile.type}`,
|
|
172
|
+
path: `auth.profiles.${name}.type`,
|
|
173
|
+
suggestion: `Use one of: ${validTypes.join(', ')}`
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Type-specific validation
|
|
177
|
+
switch (profile.type) {
|
|
178
|
+
case 'jwt':
|
|
179
|
+
if (!profile.config?.client_id && !profile.config?.token_endpoint) {
|
|
180
|
+
warnings.push({
|
|
181
|
+
code: 'QP2V009',
|
|
182
|
+
message: 'JWT profile should have client_id or token_endpoint',
|
|
183
|
+
path: `auth.profiles.${name}.config`,
|
|
184
|
+
suggestion: 'Add client_id or token_endpoint for token retrieval'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'oauth2':
|
|
189
|
+
if (!profile.config?.token_url) {
|
|
190
|
+
errors.push({
|
|
191
|
+
code: 'QP2V010',
|
|
192
|
+
message: 'OAuth2 profile requires token_url',
|
|
193
|
+
path: `auth.profiles.${name}.config.token_url`,
|
|
194
|
+
suggestion: 'Add token_url: "https://auth.example.com/oauth2/token"'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (!profile.config?.client_id) {
|
|
198
|
+
errors.push({
|
|
199
|
+
code: 'QP2V011',
|
|
200
|
+
message: 'OAuth2 profile requires client_id',
|
|
201
|
+
path: `auth.profiles.${name}.config.client_id`,
|
|
202
|
+
suggestion: 'Add client_id: "your-client-id"'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'api_key':
|
|
207
|
+
if (!profile.config?.key) {
|
|
208
|
+
errors.push({
|
|
209
|
+
code: 'QP2V012',
|
|
210
|
+
message: 'API key profile requires key',
|
|
211
|
+
path: `auth.profiles.${name}.config.key`,
|
|
212
|
+
suggestion: 'Add key or use secret reference: ${{ secrets.API_KEY }}'
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case 'bearer':
|
|
217
|
+
if (!profile.config?.token) {
|
|
218
|
+
errors.push({
|
|
219
|
+
code: 'QP2V013',
|
|
220
|
+
message: 'Bearer token profile requires token',
|
|
221
|
+
path: `auth.profiles.${name}.config.token`,
|
|
222
|
+
suggestion: 'Add token or use secret reference: ${{ secrets.BEARER_TOKEN }}'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
case 'basic':
|
|
227
|
+
if (!profile.config?.username || !profile.config?.password) {
|
|
228
|
+
warnings.push({
|
|
229
|
+
code: 'QP2V014',
|
|
230
|
+
message: 'Basic auth profile should have username and password',
|
|
231
|
+
path: `auth.profiles.${name}.config`,
|
|
232
|
+
suggestion: 'Add username and password or use secret references'
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case 'ui_login':
|
|
237
|
+
if (!profile.config?.url) {
|
|
238
|
+
errors.push({
|
|
239
|
+
code: 'QP2V015',
|
|
240
|
+
message: 'UI login profile requires url',
|
|
241
|
+
path: `auth.profiles.${name}.config.url`,
|
|
242
|
+
suggestion: 'Add url: "https://example.com/login"'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case 'totp':
|
|
247
|
+
if (!profile.config?.secret) {
|
|
248
|
+
errors.push({
|
|
249
|
+
code: 'QP2V016',
|
|
250
|
+
message: 'TOTP profile requires secret',
|
|
251
|
+
path: `auth.profiles.${name}.config.secret`,
|
|
252
|
+
suggestion: 'Add TOTP secret or use secret reference'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return { errors, warnings };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Validate gates configuration
|
|
261
|
+
*/
|
|
262
|
+
async validateGates(gates, checkFilesExist) {
|
|
263
|
+
const errors = [];
|
|
264
|
+
const warnings = [];
|
|
265
|
+
const frameworks = new Set();
|
|
266
|
+
let totalTests = 0;
|
|
267
|
+
if (!gates) {
|
|
268
|
+
return { errors, warnings, totalTests, frameworks: [] };
|
|
269
|
+
}
|
|
270
|
+
const validAdapters = [
|
|
271
|
+
'playwright-api',
|
|
272
|
+
'playwright-ui',
|
|
273
|
+
'k6-perf',
|
|
274
|
+
'semgrep-sast',
|
|
275
|
+
'zap-dast',
|
|
276
|
+
'gitleaks-secrets',
|
|
277
|
+
'osv-deps'
|
|
278
|
+
];
|
|
279
|
+
for (const [gateName, gate] of Object.entries(gates)) {
|
|
280
|
+
// Validate adapter
|
|
281
|
+
if (gate.adapter && !validAdapters.includes(gate.adapter)) {
|
|
282
|
+
warnings.push({
|
|
283
|
+
code: 'QP2V017',
|
|
284
|
+
message: `Unknown adapter: ${gate.adapter}`,
|
|
285
|
+
path: `gates.${gateName}.adapter`,
|
|
286
|
+
suggestion: `Use one of: ${validAdapters.join(', ')}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Collect adapter type
|
|
290
|
+
if (gate.adapter) {
|
|
291
|
+
frameworks.add(gate.adapter);
|
|
292
|
+
}
|
|
293
|
+
// Validate test_files
|
|
294
|
+
if (gate.test_files && gate.test_files.length > 0) {
|
|
295
|
+
for (const pattern of gate.test_files) {
|
|
296
|
+
const patternErrors = this.validateTestPattern(gateName, pattern, checkFilesExist);
|
|
297
|
+
errors.push(...patternErrors.errors);
|
|
298
|
+
warnings.push(...patternErrors.warnings);
|
|
299
|
+
// Count matching files
|
|
300
|
+
if (checkFilesExist) {
|
|
301
|
+
try {
|
|
302
|
+
const matches = await glob(pattern, { cwd: this.baseDir, absolute: false });
|
|
303
|
+
totalTests += matches.length;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Ignore glob errors
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (!gate.config && !gate.test_files) {
|
|
312
|
+
warnings.push({
|
|
313
|
+
code: 'QP2V018',
|
|
314
|
+
message: `Gate "${gateName}" has neither test_files nor config`,
|
|
315
|
+
path: `gates.${gateName}`,
|
|
316
|
+
suggestion: 'Add test_files: ["tests/**/*.spec.ts"] or config with inline configuration'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Validate auth profile reference
|
|
320
|
+
if (gate.auth) {
|
|
321
|
+
// Will be validated against profiles in auth section
|
|
322
|
+
if (!gate.adapter?.includes('api') && !gate.adapter?.includes('ui')) {
|
|
323
|
+
warnings.push({
|
|
324
|
+
code: 'QP2V019',
|
|
325
|
+
message: `Auth profile specified but adapter may not support auth`,
|
|
326
|
+
path: `gates.${gateName}.auth`,
|
|
327
|
+
suggestion: 'Ensure adapter supports authentication'
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Validate budgets
|
|
332
|
+
if (gate.budgets) {
|
|
333
|
+
const budgetErrors = this.validateGateBudgets(gateName, gate.budgets);
|
|
334
|
+
errors.push(...budgetErrors);
|
|
335
|
+
}
|
|
336
|
+
// Validate dependencies
|
|
337
|
+
if (gate.depends_on) {
|
|
338
|
+
for (const dep of gate.depends_on) {
|
|
339
|
+
if (!gates[dep]) {
|
|
340
|
+
errors.push({
|
|
341
|
+
code: 'QP2V020',
|
|
342
|
+
message: `Gate depends on non-existent gate: ${dep}`,
|
|
343
|
+
path: `gates.${gateName}.depends_on`,
|
|
344
|
+
suggestion: `Remove ${dep} from dependencies or define the gate`
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { errors, warnings, totalTests, frameworks: Array.from(frameworks) };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Validate a test file pattern
|
|
354
|
+
*/
|
|
355
|
+
validateTestPattern(gateName, pattern, checkExistence) {
|
|
356
|
+
const errors = [];
|
|
357
|
+
const warnings = [];
|
|
358
|
+
// Check for invalid patterns
|
|
359
|
+
if (pattern.includes('..')) {
|
|
360
|
+
errors.push({
|
|
361
|
+
code: 'QP2V021',
|
|
362
|
+
message: 'Test pattern cannot contain parent directory reference ".."',
|
|
363
|
+
path: `gates.${gateName}.test_files`,
|
|
364
|
+
suggestion: 'Use relative patterns without ..'
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (pattern.startsWith('/')) {
|
|
368
|
+
warnings.push({
|
|
369
|
+
code: 'QP2V022',
|
|
370
|
+
message: 'Absolute path may not work across environments',
|
|
371
|
+
path: `gates.${gateName}.test_files`,
|
|
372
|
+
suggestion: 'Use relative pattern from pack file location'
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return { errors, warnings };
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Validate gate budgets
|
|
379
|
+
*/
|
|
380
|
+
validateGateBudgets(gateName, budgets) {
|
|
381
|
+
const errors = [];
|
|
382
|
+
for (const [key, value] of Object.entries(budgets)) {
|
|
383
|
+
if (typeof value !== 'number' || value < 0) {
|
|
384
|
+
errors.push({
|
|
385
|
+
code: 'QP2V023',
|
|
386
|
+
message: `Budget value must be a positive number: ${key}`,
|
|
387
|
+
path: `gates.${gateName}.budgets.${key}`,
|
|
388
|
+
suggestion: 'Use a positive number value'
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return errors;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Validate hooks configuration
|
|
396
|
+
*/
|
|
397
|
+
validateHooks(hooks) {
|
|
398
|
+
const errors = [];
|
|
399
|
+
const warnings = [];
|
|
400
|
+
if (!hooks) {
|
|
401
|
+
return { errors, warnings };
|
|
402
|
+
}
|
|
403
|
+
const validHookTypes = ['run', 'wait_on', 'script', 'docker'];
|
|
404
|
+
const validPhases = ['beforeAll', 'afterAll', 'beforeEach', 'afterEach'];
|
|
405
|
+
for (const [phase, hookList] of Object.entries(hooks)) {
|
|
406
|
+
if (!validPhases.includes(phase)) {
|
|
407
|
+
warnings.push({
|
|
408
|
+
code: 'QP2V024',
|
|
409
|
+
message: `Unknown hook phase: ${phase}`,
|
|
410
|
+
path: `hooks.${phase}`,
|
|
411
|
+
suggestion: `Use one of: ${validPhases.join(', ')}`
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(hookList)) {
|
|
416
|
+
errors.push({
|
|
417
|
+
code: 'QP2V025',
|
|
418
|
+
message: `Hook phase must be an array: ${phase}`,
|
|
419
|
+
path: `hooks.${phase}`,
|
|
420
|
+
suggestion: 'Use array format: hooks: { beforeAll: [{ run: "command" }] }'
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
425
|
+
const hook = hookList[i];
|
|
426
|
+
const hookErrors = this.validateHook(phase, i, hook, validHookTypes);
|
|
427
|
+
errors.push(...hookErrors.errors);
|
|
428
|
+
warnings.push(...hookErrors.warnings);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { errors, warnings };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Validate a single hook
|
|
435
|
+
*/
|
|
436
|
+
validateHook(phase, index, hook, validTypes) {
|
|
437
|
+
const errors = [];
|
|
438
|
+
const warnings = [];
|
|
439
|
+
if (!hook.type || !validTypes.includes(hook.type)) {
|
|
440
|
+
errors.push({
|
|
441
|
+
code: 'QP2V026',
|
|
442
|
+
message: `Invalid or missing hook type: ${hook.type}`,
|
|
443
|
+
path: `hooks.${phase}[${index}].type`,
|
|
444
|
+
suggestion: `Use one of: ${validTypes.join(', ')}`
|
|
445
|
+
});
|
|
446
|
+
return { errors, warnings };
|
|
447
|
+
}
|
|
448
|
+
// Type-specific validation
|
|
449
|
+
switch (hook.type) {
|
|
450
|
+
case 'run':
|
|
451
|
+
case 'script':
|
|
452
|
+
if (!hook.command) {
|
|
453
|
+
errors.push({
|
|
454
|
+
code: 'QP2V027',
|
|
455
|
+
message: `${hook.type} hook requires command`,
|
|
456
|
+
path: `hooks.${phase}[${index}].command`,
|
|
457
|
+
suggestion: 'Add command to execute'
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
case 'wait_on':
|
|
462
|
+
if (!hook.wait_for?.resource) {
|
|
463
|
+
errors.push({
|
|
464
|
+
code: 'QP2V028',
|
|
465
|
+
message: 'wait_on hook requires resource',
|
|
466
|
+
path: `hooks.${phase}[${index}].wait_for.resource`,
|
|
467
|
+
suggestion: 'Add resource: "http://localhost:3000" or "tcp://localhost:8080"'
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
case 'docker':
|
|
472
|
+
if (!hook.compose) {
|
|
473
|
+
warnings.push({
|
|
474
|
+
code: 'QP2V029',
|
|
475
|
+
message: 'Docker hook should have compose configuration',
|
|
476
|
+
path: `hooks.${phase}[${index}]`,
|
|
477
|
+
suggestion: 'Add compose: { services: ["app"] }'
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
// Check for dangerous commands
|
|
483
|
+
if (hook.command) {
|
|
484
|
+
const dangerous = ['rm -rf', 'sudo rm', 'del /f', 'format', 'mkfs'];
|
|
485
|
+
if (dangerous.some(d => hook.command.toLowerCase().includes(d))) {
|
|
486
|
+
warnings.push({
|
|
487
|
+
code: 'QP2V030',
|
|
488
|
+
message: 'Potentially dangerous command in hook',
|
|
489
|
+
path: `hooks.${phase}[${index}].command`,
|
|
490
|
+
suggestion: 'Review command for safety'
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { errors, warnings };
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Validate execution configuration
|
|
498
|
+
*/
|
|
499
|
+
validateExecutionConfig(execution) {
|
|
500
|
+
const errors = [];
|
|
501
|
+
const warnings = [];
|
|
502
|
+
if (!execution) {
|
|
503
|
+
// Add default if missing
|
|
504
|
+
return { errors, warnings };
|
|
505
|
+
}
|
|
506
|
+
// Validate on_failure
|
|
507
|
+
if (execution.on_failure && !['stop', 'continue', 'proceed'].includes(execution.on_failure)) {
|
|
508
|
+
errors.push({
|
|
509
|
+
code: 'QP2V031',
|
|
510
|
+
message: `Invalid on_failure value: ${execution.on_failure}`,
|
|
511
|
+
path: 'execution.on_failure',
|
|
512
|
+
suggestion: 'Use one of: stop, continue, proceed'
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// Validate numeric values
|
|
516
|
+
if (execution.default_timeout !== undefined && (typeof execution.default_timeout !== 'number' || execution.default_timeout <= 0)) {
|
|
517
|
+
errors.push({
|
|
518
|
+
code: 'QP2V032',
|
|
519
|
+
message: 'default_timeout must be a positive number',
|
|
520
|
+
path: 'execution.default_timeout',
|
|
521
|
+
suggestion: 'Use timeout in milliseconds (e.g., 30000 for 30s)'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (execution.max_concurrency !== undefined && (typeof execution.max_concurrency !== 'number' || execution.max_concurrency <= 0)) {
|
|
525
|
+
errors.push({
|
|
526
|
+
code: 'QP2V033',
|
|
527
|
+
message: 'max_concurrency must be a positive number',
|
|
528
|
+
path: 'execution.max_concurrency',
|
|
529
|
+
suggestion: 'Use a value >= 1'
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return { errors, warnings };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Validate business rules
|
|
536
|
+
*/
|
|
537
|
+
validateBusinessRules(pack) {
|
|
538
|
+
const errors = [];
|
|
539
|
+
const warnings = [];
|
|
540
|
+
// Early return if no gates (already caught by required validation)
|
|
541
|
+
if (!pack.gates || Object.keys(pack.gates).length === 0) {
|
|
542
|
+
return { errors, warnings };
|
|
543
|
+
}
|
|
544
|
+
// Check for API gate with api auth but no auth profile
|
|
545
|
+
const apiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('api'));
|
|
546
|
+
if (apiGates.length > 0 && !pack.auth?.api && !pack.auth?.profiles) {
|
|
547
|
+
warnings.push({
|
|
548
|
+
code: 'QP2V034',
|
|
549
|
+
message: 'API gates defined but no default auth profile configured',
|
|
550
|
+
path: 'auth.api',
|
|
551
|
+
suggestion: 'Add auth.api or define profiles for API authentication'
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
// Check for UI gate with ui auth but no auth profile
|
|
555
|
+
const uiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('ui'));
|
|
556
|
+
if (uiGates.length > 0 && !pack.auth?.ui && !pack.auth?.profiles) {
|
|
557
|
+
warnings.push({
|
|
558
|
+
code: 'QP2V035',
|
|
559
|
+
message: 'UI gates defined but no default auth profile configured',
|
|
560
|
+
path: 'auth.ui',
|
|
561
|
+
suggestion: 'Add auth.ui or define profiles for UI authentication'
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
// Check for gates without adapter
|
|
565
|
+
const gatesWithoutAdapter = Object.entries(pack.gates).filter(([_, g]) => !g.adapter);
|
|
566
|
+
if (gatesWithoutAdapter.length > 0) {
|
|
567
|
+
const names = gatesWithoutAdapter.map(([name]) => name).join(', ');
|
|
568
|
+
warnings.push({
|
|
569
|
+
code: 'QP2V036',
|
|
570
|
+
message: `Gates without adapter: ${names}`,
|
|
571
|
+
path: 'gates',
|
|
572
|
+
suggestion: 'Add adapter to each gate for proper execution'
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return { errors, warnings };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects regressions using statistical methods.
|
|
5
|
+
* Supports Z-score, T-test, Mann-Whitney U, and percentile-based detection.
|
|
6
|
+
*/
|
|
7
|
+
import type { RegressionDetection, RegressionConfig, TimeSeriesPoint, Baseline } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Regression Detector class
|
|
10
|
+
*/
|
|
11
|
+
export declare class RegressionDetector {
|
|
12
|
+
private config;
|
|
13
|
+
private baselines;
|
|
14
|
+
constructor(config?: Partial<RegressionConfig>);
|
|
15
|
+
/**
|
|
16
|
+
* Detect regressions by comparing current values to baseline
|
|
17
|
+
*/
|
|
18
|
+
detectRegressions(currentData: Record<string, number>, currentRunId: string, gate: string, context?: Record<string, unknown>): RegressionDetection[];
|
|
19
|
+
/**
|
|
20
|
+
* Detect regression for a single metric
|
|
21
|
+
*/
|
|
22
|
+
private detectMetricRegression;
|
|
23
|
+
/**
|
|
24
|
+
* Perform statistical test
|
|
25
|
+
*/
|
|
26
|
+
private performStatisticalTest;
|
|
27
|
+
/**
|
|
28
|
+
* Z-score test
|
|
29
|
+
*/
|
|
30
|
+
private zScoreTest;
|
|
31
|
+
/**
|
|
32
|
+
* T-test (simplified for single value vs baseline)
|
|
33
|
+
*/
|
|
34
|
+
private tTest;
|
|
35
|
+
/**
|
|
36
|
+
* Percentile test
|
|
37
|
+
*/
|
|
38
|
+
private percentileTest;
|
|
39
|
+
/**
|
|
40
|
+
* Calculate percentile for a value in baseline
|
|
41
|
+
*/
|
|
42
|
+
private calculatePercentile;
|
|
43
|
+
/**
|
|
44
|
+
* Normal CDF (cumulative distribution function)
|
|
45
|
+
*/
|
|
46
|
+
private normalCDF;
|
|
47
|
+
/**
|
|
48
|
+
* Student's t CDF approximation
|
|
49
|
+
*/
|
|
50
|
+
private studentTCDF;
|
|
51
|
+
/**
|
|
52
|
+
* Check if change constitutes a regression
|
|
53
|
+
*/
|
|
54
|
+
private isRegression;
|
|
55
|
+
/**
|
|
56
|
+
* Get direction of change (worse/better/neutral)
|
|
57
|
+
*/
|
|
58
|
+
private getDIRECTION;
|
|
59
|
+
/**
|
|
60
|
+
* Calculate regression severity
|
|
61
|
+
*/
|
|
62
|
+
private calculateSeverity;
|
|
63
|
+
/**
|
|
64
|
+
* Generate suggestions for regression
|
|
65
|
+
*/
|
|
66
|
+
private generateSuggestions;
|
|
67
|
+
/**
|
|
68
|
+
* Update baseline with new data
|
|
69
|
+
*/
|
|
70
|
+
updateBaseline(metricName: string, data: TimeSeriesPoint[]): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get baseline for a metric
|
|
73
|
+
*/
|
|
74
|
+
getBaseline(metricName: string): Baseline | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Get all baselines
|
|
77
|
+
*/
|
|
78
|
+
getAllBaselines(): Baseline[];
|
|
79
|
+
/**
|
|
80
|
+
* Get threshold for a metric
|
|
81
|
+
*/
|
|
82
|
+
private getThresholdForMetric;
|
|
83
|
+
/**
|
|
84
|
+
* Check if metric matches pattern
|
|
85
|
+
*/
|
|
86
|
+
private metricMatchesPattern;
|
|
87
|
+
/**
|
|
88
|
+
* Get regression type for metric
|
|
89
|
+
*/
|
|
90
|
+
private getTypeForMetric;
|
|
91
|
+
/**
|
|
92
|
+
* Extract component name from metric and context
|
|
93
|
+
*/
|
|
94
|
+
private extractComponent;
|
|
95
|
+
/**
|
|
96
|
+
* Check if metric should be ignored
|
|
97
|
+
*/
|
|
98
|
+
private shouldIgnore;
|
|
99
|
+
/**
|
|
100
|
+
* Create default configuration
|
|
101
|
+
*/
|
|
102
|
+
private createDefaultConfig;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a regression detector
|
|
106
|
+
*/
|
|
107
|
+
export declare function createRegressionDetector(config?: Partial<RegressionConfig>): RegressionDetector;
|