@arclabs561/ai-visual-test 0.5.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/.secretsignore.example +20 -0
- package/CHANGELOG.md +360 -0
- package/CONTRIBUTING.md +63 -0
- package/DEPLOYMENT.md +80 -0
- package/LICENSE +22 -0
- package/README.md +142 -0
- package/SECURITY.md +108 -0
- package/api/health.js +34 -0
- package/api/validate.js +252 -0
- package/index.d.ts +1221 -0
- package/package.json +112 -0
- package/public/index.html +149 -0
- package/src/batch-optimizer.mjs +451 -0
- package/src/bias-detector.mjs +370 -0
- package/src/bias-mitigation.mjs +233 -0
- package/src/cache.mjs +433 -0
- package/src/config.mjs +268 -0
- package/src/constants.mjs +80 -0
- package/src/context-compressor.mjs +350 -0
- package/src/convenience.mjs +617 -0
- package/src/cost-tracker.mjs +257 -0
- package/src/cross-modal-consistency.mjs +170 -0
- package/src/data-extractor.mjs +232 -0
- package/src/dynamic-few-shot.mjs +140 -0
- package/src/dynamic-prompts.mjs +361 -0
- package/src/ensemble/index.mjs +53 -0
- package/src/ensemble-judge.mjs +366 -0
- package/src/error-handler.mjs +67 -0
- package/src/errors.mjs +167 -0
- package/src/experience-propagation.mjs +128 -0
- package/src/experience-tracer.mjs +487 -0
- package/src/explanation-manager.mjs +299 -0
- package/src/feedback-aggregator.mjs +248 -0
- package/src/game-goal-prompts.mjs +478 -0
- package/src/game-player.mjs +548 -0
- package/src/hallucination-detector.mjs +155 -0
- package/src/helpers/playwright.mjs +80 -0
- package/src/human-validation-manager.mjs +516 -0
- package/src/index.mjs +364 -0
- package/src/judge.mjs +929 -0
- package/src/latency-aware-batch-optimizer.mjs +192 -0
- package/src/load-env.mjs +159 -0
- package/src/logger.mjs +55 -0
- package/src/metrics.mjs +187 -0
- package/src/model-tier-selector.mjs +221 -0
- package/src/multi-modal/index.mjs +36 -0
- package/src/multi-modal-fusion.mjs +190 -0
- package/src/multi-modal.mjs +524 -0
- package/src/natural-language-specs.mjs +1071 -0
- package/src/pair-comparison.mjs +277 -0
- package/src/persona/index.mjs +42 -0
- package/src/persona-enhanced.mjs +200 -0
- package/src/persona-experience.mjs +572 -0
- package/src/position-counterbalance.mjs +140 -0
- package/src/prompt-composer.mjs +375 -0
- package/src/render-change-detector.mjs +583 -0
- package/src/research-enhanced-validation.mjs +436 -0
- package/src/retry.mjs +152 -0
- package/src/rubrics.mjs +231 -0
- package/src/score-tracker.mjs +277 -0
- package/src/smart-validator.mjs +447 -0
- package/src/spec-config.mjs +106 -0
- package/src/spec-templates.mjs +347 -0
- package/src/specs/index.mjs +38 -0
- package/src/temporal/index.mjs +102 -0
- package/src/temporal-adaptive.mjs +163 -0
- package/src/temporal-batch-optimizer.mjs +222 -0
- package/src/temporal-constants.mjs +69 -0
- package/src/temporal-context.mjs +49 -0
- package/src/temporal-decision-manager.mjs +271 -0
- package/src/temporal-decision.mjs +669 -0
- package/src/temporal-errors.mjs +58 -0
- package/src/temporal-note-pruner.mjs +173 -0
- package/src/temporal-preprocessor.mjs +543 -0
- package/src/temporal-prompt-formatter.mjs +219 -0
- package/src/temporal-validation.mjs +159 -0
- package/src/temporal.mjs +415 -0
- package/src/type-guards.mjs +311 -0
- package/src/uncertainty-reducer.mjs +470 -0
- package/src/utils/index.mjs +175 -0
- package/src/validation-framework.mjs +321 -0
- package/src/validation-result-normalizer.mjs +64 -0
- package/src/validation.mjs +243 -0
- package/src/validators/accessibility-programmatic.mjs +345 -0
- package/src/validators/accessibility-validator.mjs +223 -0
- package/src/validators/batch-validator.mjs +143 -0
- package/src/validators/hybrid-validator.mjs +268 -0
- package/src/validators/index.mjs +34 -0
- package/src/validators/prompt-builder.mjs +218 -0
- package/src/validators/rubric.mjs +85 -0
- package/src/validators/state-programmatic.mjs +260 -0
- package/src/validators/state-validator.mjs +291 -0
- package/vercel.json +27 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Validator Selector
|
|
3
|
+
*
|
|
4
|
+
* Automatically selects the best validator type based on available context.
|
|
5
|
+
* Guides users to the right tool for the job.
|
|
6
|
+
*
|
|
7
|
+
* Design Philosophy:
|
|
8
|
+
* - If you have page access and can measure it programmatically → use programmatic
|
|
9
|
+
* - If you only have screenshots and need semantic evaluation → use VLLM
|
|
10
|
+
* - If you need both accuracy and semantic understanding → use hybrid
|
|
11
|
+
*
|
|
12
|
+
* This prevents the common mistake of using VLLM for measurable things.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ValidationError } from './errors.mjs';
|
|
16
|
+
import {
|
|
17
|
+
checkElementContrast,
|
|
18
|
+
checkAllTextContrast,
|
|
19
|
+
checkKeyboardNavigation
|
|
20
|
+
} from './validators/accessibility-programmatic.mjs';
|
|
21
|
+
import {
|
|
22
|
+
validateStateProgrammatic as validateStateProg,
|
|
23
|
+
validateElementPosition as validatePosProg
|
|
24
|
+
} from './validators/state-programmatic.mjs';
|
|
25
|
+
import {
|
|
26
|
+
validateAccessibilityHybrid,
|
|
27
|
+
validateStateHybrid
|
|
28
|
+
} from './validators/hybrid-validator.mjs';
|
|
29
|
+
import {
|
|
30
|
+
AccessibilityValidator,
|
|
31
|
+
StateValidator
|
|
32
|
+
} from './validators/index.mjs';
|
|
33
|
+
import { validateScreenshot } from './judge.mjs';
|
|
34
|
+
import { log, warn } from './logger.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Smart accessibility validation
|
|
38
|
+
*
|
|
39
|
+
* Automatically chooses the best validator based on available context:
|
|
40
|
+
* - Has page access → uses programmatic (fast, deterministic)
|
|
41
|
+
* - Only has screenshot → uses VLLM (semantic evaluation)
|
|
42
|
+
* - Has both and needs semantic context → uses hybrid (best of both)
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} options - Validation options
|
|
45
|
+
* @param {any} [options.page] - Playwright page object (if available)
|
|
46
|
+
* @param {string} [options.screenshotPath] - Path to screenshot (if available)
|
|
47
|
+
* @param {string} [options.selector] - CSS selector for element (if checking specific element)
|
|
48
|
+
* @param {number} [options.minContrast] - Minimum contrast ratio (default: 4.5)
|
|
49
|
+
* @param {boolean} [options.useHybrid] - Force hybrid validation (default: auto-detect)
|
|
50
|
+
* @param {boolean} [options.needSemantic] - Need semantic evaluation (default: false)
|
|
51
|
+
* @returns {Promise<Object>} Validation result
|
|
52
|
+
*/
|
|
53
|
+
export async function validateAccessibilitySmart(options = {}) {
|
|
54
|
+
const {
|
|
55
|
+
page = null,
|
|
56
|
+
screenshotPath = null,
|
|
57
|
+
selector = null,
|
|
58
|
+
minContrast = 4.5,
|
|
59
|
+
useHybrid = null, // null = auto-detect
|
|
60
|
+
needSemantic = false
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
// Validate inputs
|
|
64
|
+
if (!page && !screenshotPath) {
|
|
65
|
+
throw new ValidationError(
|
|
66
|
+
'validateAccessibilitySmart: Either page or screenshotPath is required',
|
|
67
|
+
{ options }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Auto-detect: use hybrid if both available and semantic needed
|
|
72
|
+
const shouldUseHybrid = useHybrid !== null
|
|
73
|
+
? useHybrid
|
|
74
|
+
: (page && screenshotPath && needSemantic);
|
|
75
|
+
|
|
76
|
+
// Decision tree:
|
|
77
|
+
// 1. Has page access → use programmatic (fast, deterministic)
|
|
78
|
+
// 2. Has both + need semantic → use hybrid (best of both)
|
|
79
|
+
// 3. Only screenshot → use VLLM (semantic evaluation)
|
|
80
|
+
|
|
81
|
+
if (page && !shouldUseHybrid) {
|
|
82
|
+
// Use programmatic validator (fast, deterministic)
|
|
83
|
+
log('[SmartValidator] Using programmatic accessibility validator (fast, deterministic)');
|
|
84
|
+
|
|
85
|
+
if (selector) {
|
|
86
|
+
// Check specific element
|
|
87
|
+
return await checkElementContrast(page, selector, minContrast);
|
|
88
|
+
} else {
|
|
89
|
+
// Check all text elements
|
|
90
|
+
const contrast = await checkAllTextContrast(page, minContrast);
|
|
91
|
+
const keyboard = await checkKeyboardNavigation(page);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
contrast,
|
|
95
|
+
keyboard,
|
|
96
|
+
method: 'programmatic',
|
|
97
|
+
speed: 'fast',
|
|
98
|
+
cost: 'free'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
} else if (page && screenshotPath && shouldUseHybrid) {
|
|
102
|
+
// Use hybrid validator (programmatic + VLLM)
|
|
103
|
+
log('[SmartValidator] Using hybrid accessibility validator (programmatic + VLLM)');
|
|
104
|
+
|
|
105
|
+
return await validateAccessibilityHybrid(
|
|
106
|
+
page,
|
|
107
|
+
screenshotPath,
|
|
108
|
+
minContrast,
|
|
109
|
+
{ testType: 'accessibility-smart' }
|
|
110
|
+
);
|
|
111
|
+
} else if (screenshotPath) {
|
|
112
|
+
// Use VLLM validator (semantic evaluation)
|
|
113
|
+
log('[SmartValidator] Using VLLM accessibility validator (semantic evaluation)');
|
|
114
|
+
warn('[SmartValidator] Consider using programmatic validator if you have page access (faster, more reliable)');
|
|
115
|
+
|
|
116
|
+
const validator = new AccessibilityValidator({
|
|
117
|
+
minContrast,
|
|
118
|
+
standards: ['WCAG-AA']
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return await validator.validateAccessibility(screenshotPath, {
|
|
122
|
+
testType: 'accessibility-smart'
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
throw new ValidationError(
|
|
126
|
+
'validateAccessibilitySmart: Invalid combination of options',
|
|
127
|
+
{ page: !!page, screenshotPath: !!screenshotPath }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Smart state validation
|
|
134
|
+
*
|
|
135
|
+
* Automatically chooses the best validator based on available context:
|
|
136
|
+
* - Has page access + direct state → uses programmatic (fast, deterministic)
|
|
137
|
+
* - Has page access + screenshot + need semantic → uses hybrid (best of both)
|
|
138
|
+
* - Only screenshot → uses VLLM (extracts state from screenshot)
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} options - Validation options
|
|
141
|
+
* @param {any} [options.page] - Playwright page object (if available)
|
|
142
|
+
* @param {string} [options.screenshotPath] - Path to screenshot (if available)
|
|
143
|
+
* @param {Object} options.expectedState - Expected state object
|
|
144
|
+
* @param {Object} [options.selectors] - Map of state keys to CSS selectors
|
|
145
|
+
* @param {number} [options.tolerance] - Pixel tolerance (default: 5)
|
|
146
|
+
* @param {boolean} [options.useHybrid] - Force hybrid validation (default: auto-detect)
|
|
147
|
+
* @param {boolean} [options.needSemantic] - Need semantic evaluation (default: false)
|
|
148
|
+
* @returns {Promise<Object>} Validation result
|
|
149
|
+
*/
|
|
150
|
+
export async function validateStateSmart(options = {}) {
|
|
151
|
+
const {
|
|
152
|
+
page = null,
|
|
153
|
+
screenshotPath = null,
|
|
154
|
+
expectedState,
|
|
155
|
+
selectors = {},
|
|
156
|
+
tolerance = 5,
|
|
157
|
+
useHybrid = null, // null = auto-detect
|
|
158
|
+
needSemantic = false
|
|
159
|
+
} = options;
|
|
160
|
+
|
|
161
|
+
// Validate inputs
|
|
162
|
+
if (!expectedState || typeof expectedState !== 'object') {
|
|
163
|
+
throw new ValidationError(
|
|
164
|
+
'validateStateSmart: expectedState is required and must be an object',
|
|
165
|
+
{ received: typeof expectedState }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!page && !screenshotPath) {
|
|
170
|
+
throw new ValidationError(
|
|
171
|
+
'validateStateSmart: Either page or screenshotPath is required',
|
|
172
|
+
{ options }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if we have direct state access (window.gameState, etc.)
|
|
177
|
+
let hasDirectState = false;
|
|
178
|
+
if (page) {
|
|
179
|
+
try {
|
|
180
|
+
const gameState = await page.evaluate(() => window.gameState || null);
|
|
181
|
+
hasDirectState = gameState !== null;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Ignore - no direct state access
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Auto-detect: use hybrid if both available and semantic needed
|
|
188
|
+
const shouldUseHybrid = useHybrid !== null
|
|
189
|
+
? useHybrid
|
|
190
|
+
: (page && screenshotPath && needSemantic);
|
|
191
|
+
|
|
192
|
+
// Decision tree:
|
|
193
|
+
// 1. Has page access + direct state → use programmatic (fast, deterministic)
|
|
194
|
+
// 2. Has page access + selectors → use programmatic (fast, deterministic)
|
|
195
|
+
// 3. Has both + need semantic → use hybrid (best of both)
|
|
196
|
+
// 4. Only screenshot → use VLLM (extracts state from screenshot)
|
|
197
|
+
|
|
198
|
+
if (page && (hasDirectState || Object.keys(selectors).length > 0) && !shouldUseHybrid) {
|
|
199
|
+
// Use programmatic validator (fast, deterministic)
|
|
200
|
+
log('[SmartValidator] Using programmatic state validator (fast, deterministic)');
|
|
201
|
+
|
|
202
|
+
return await validateStateProg(
|
|
203
|
+
page,
|
|
204
|
+
expectedState,
|
|
205
|
+
{ selectors, tolerance }
|
|
206
|
+
);
|
|
207
|
+
} else if (page && screenshotPath && shouldUseHybrid) {
|
|
208
|
+
// Use hybrid validator (programmatic + VLLM)
|
|
209
|
+
log('[SmartValidator] Using hybrid state validator (programmatic + VLLM)');
|
|
210
|
+
|
|
211
|
+
return await validateStateHybrid(
|
|
212
|
+
page,
|
|
213
|
+
screenshotPath,
|
|
214
|
+
expectedState,
|
|
215
|
+
{ selectors, tolerance, testType: 'state-smart' }
|
|
216
|
+
);
|
|
217
|
+
} else if (screenshotPath) {
|
|
218
|
+
// Use VLLM validator (extracts state from screenshot)
|
|
219
|
+
log('[SmartValidator] Using VLLM state validator (extracts state from screenshot)');
|
|
220
|
+
warn('[SmartValidator] Consider using programmatic validator if you have page access (faster, more reliable)');
|
|
221
|
+
|
|
222
|
+
const validator = new StateValidator({ tolerance });
|
|
223
|
+
return await validator.validateState(
|
|
224
|
+
screenshotPath,
|
|
225
|
+
expectedState,
|
|
226
|
+
{ testType: 'state-smart' }
|
|
227
|
+
);
|
|
228
|
+
} else {
|
|
229
|
+
throw new ValidationError(
|
|
230
|
+
'validateStateSmart: Invalid combination of options',
|
|
231
|
+
{ page: !!page, screenshotPath: !!screenshotPath, hasDirectState, selectors: Object.keys(selectors).length }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Smart element validation
|
|
238
|
+
*
|
|
239
|
+
* Validates element visibility, position, contrast, etc. using the best available method.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object} options - Validation options
|
|
242
|
+
* @param {any} options.page - Playwright page object
|
|
243
|
+
* @param {string} options.selector - CSS selector for element
|
|
244
|
+
* @param {Object} [options.checks] - What to check: { visible, position, contrast }
|
|
245
|
+
* @param {Object} [options.expectedPosition] - Expected position {x, y, width, height}
|
|
246
|
+
* @param {number} [options.minContrast] - Minimum contrast ratio
|
|
247
|
+
* @param {number} [options.tolerance] - Pixel tolerance for position (default: 5)
|
|
248
|
+
* @returns {Promise<Object>} Validation result
|
|
249
|
+
*/
|
|
250
|
+
export async function validateElementSmart(options = {}) {
|
|
251
|
+
const {
|
|
252
|
+
page,
|
|
253
|
+
selector,
|
|
254
|
+
checks = { visible: true, position: false, contrast: false },
|
|
255
|
+
expectedPosition = null,
|
|
256
|
+
minContrast = 4.5,
|
|
257
|
+
tolerance = 5
|
|
258
|
+
} = options;
|
|
259
|
+
|
|
260
|
+
if (!page || typeof page.evaluate !== 'function') {
|
|
261
|
+
throw new ValidationError(
|
|
262
|
+
'validateElementSmart: page is required and must be a Playwright Page object',
|
|
263
|
+
{ received: typeof page }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!selector || typeof selector !== 'string') {
|
|
268
|
+
throw new ValidationError(
|
|
269
|
+
'validateElementSmart: selector is required and must be a string',
|
|
270
|
+
{ received: typeof selector }
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const results = {};
|
|
275
|
+
|
|
276
|
+
// Always use programmatic (we have page access)
|
|
277
|
+
if (checks.visible) {
|
|
278
|
+
const visible = await page.locator(selector).isVisible();
|
|
279
|
+
results.visible = visible;
|
|
280
|
+
if (!visible) {
|
|
281
|
+
results.errors = results.errors || [];
|
|
282
|
+
results.errors.push(`Element ${selector} is not visible`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (checks.position && expectedPosition) {
|
|
287
|
+
const position = await validatePosProg(
|
|
288
|
+
page,
|
|
289
|
+
selector,
|
|
290
|
+
expectedPosition,
|
|
291
|
+
tolerance
|
|
292
|
+
);
|
|
293
|
+
results.position = position;
|
|
294
|
+
if (!position.matches) {
|
|
295
|
+
results.errors = results.errors || [];
|
|
296
|
+
results.errors.push(`Element ${selector} position mismatch: ${position.diff}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (checks.contrast) {
|
|
301
|
+
const contrast = await checkElementContrast(page, selector, minContrast);
|
|
302
|
+
results.contrast = contrast;
|
|
303
|
+
if (!contrast.passes) {
|
|
304
|
+
results.errors = results.errors || [];
|
|
305
|
+
results.errors.push(`Element ${selector} contrast ${contrast.ratio}:1 < ${minContrast}:1`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
results.passes = !results.errors || results.errors.length === 0;
|
|
310
|
+
results.method = 'programmatic';
|
|
311
|
+
results.speed = 'fast';
|
|
312
|
+
results.cost = 'free';
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Smart validation with automatic tool selection
|
|
319
|
+
*
|
|
320
|
+
* This is the main entry point that automatically selects the best validator
|
|
321
|
+
* based on what you're trying to validate and what context you have.
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} options - Validation options
|
|
324
|
+
* @param {string} options.type - Type of validation: 'accessibility', 'state', 'element', 'visual'
|
|
325
|
+
* @param {any} [options.page] - Playwright page object (if available)
|
|
326
|
+
* @param {string} [options.screenshotPath] - Path to screenshot (if available)
|
|
327
|
+
* @param {Object} [options.expectedState] - Expected state (for state validation)
|
|
328
|
+
* @param {string} [options.selector] - CSS selector (for element validation)
|
|
329
|
+
* @param {Object} [options.checks] - What to check (for element validation)
|
|
330
|
+
* @param {string} [options.prompt] - Evaluation prompt (for visual validation)
|
|
331
|
+
* @param {Object} [options.context] - Additional context
|
|
332
|
+
* @returns {Promise<Object>} Validation result
|
|
333
|
+
*/
|
|
334
|
+
export async function validateSmart(options = {}) {
|
|
335
|
+
const { type, ...rest } = options;
|
|
336
|
+
|
|
337
|
+
if (!type) {
|
|
338
|
+
throw new ValidationError(
|
|
339
|
+
'validateSmart: type is required (accessibility, state, element, or visual)',
|
|
340
|
+
{ received: type }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
switch (type) {
|
|
345
|
+
case 'accessibility':
|
|
346
|
+
return await validateAccessibilitySmart(rest);
|
|
347
|
+
|
|
348
|
+
case 'state':
|
|
349
|
+
return await validateStateSmart(rest);
|
|
350
|
+
|
|
351
|
+
case 'element':
|
|
352
|
+
return await validateElementSmart(rest);
|
|
353
|
+
|
|
354
|
+
case 'visual':
|
|
355
|
+
// For visual validation, always use VLLM (semantic evaluation)
|
|
356
|
+
if (!rest.screenshotPath) {
|
|
357
|
+
throw new ValidationError(
|
|
358
|
+
'validateSmart: screenshotPath is required for visual validation',
|
|
359
|
+
{ type }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (!rest.prompt) {
|
|
363
|
+
throw new ValidationError(
|
|
364
|
+
'validateSmart: prompt is required for visual validation',
|
|
365
|
+
{ type }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
log('[SmartValidator] Using VLLM for visual validation (semantic evaluation)');
|
|
369
|
+
return await validateScreenshot(
|
|
370
|
+
rest.screenshotPath,
|
|
371
|
+
rest.prompt,
|
|
372
|
+
{ testType: 'visual-smart', ...rest.context }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
throw new ValidationError(
|
|
377
|
+
`validateSmart: Unknown type "${type}" (must be: accessibility, state, element, or visual)`,
|
|
378
|
+
{ received: type }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Helper to detect if validation can be done programmatically
|
|
385
|
+
*
|
|
386
|
+
* @param {Object} options - Validation options
|
|
387
|
+
* @param {any} [options.page] - Playwright page object
|
|
388
|
+
* @param {string} [options.screenshotPath] - Path to screenshot
|
|
389
|
+
* @param {string} [options.type] - Type of validation
|
|
390
|
+
* @returns {Object} Detection result with recommendations
|
|
391
|
+
*/
|
|
392
|
+
export function detectValidationMethod(options = {}) {
|
|
393
|
+
const { page, screenshotPath, type } = options;
|
|
394
|
+
|
|
395
|
+
const hasPage = page && typeof page.evaluate === 'function';
|
|
396
|
+
const hasScreenshot = !!screenshotPath;
|
|
397
|
+
|
|
398
|
+
const recommendations = [];
|
|
399
|
+
|
|
400
|
+
if (type === 'accessibility' || type === 'state') {
|
|
401
|
+
if (hasPage) {
|
|
402
|
+
recommendations.push({
|
|
403
|
+
method: 'programmatic',
|
|
404
|
+
reason: 'Has page access - use programmatic validator (fast, deterministic, free)',
|
|
405
|
+
speed: 'fast',
|
|
406
|
+
cost: 'free',
|
|
407
|
+
reliability: 'high'
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (hasPage && hasScreenshot) {
|
|
412
|
+
recommendations.push({
|
|
413
|
+
method: 'hybrid',
|
|
414
|
+
reason: 'Has both page and screenshot - use hybrid validator (programmatic ground truth + VLLM semantic)',
|
|
415
|
+
speed: 'medium',
|
|
416
|
+
cost: 'api',
|
|
417
|
+
reliability: 'high'
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (hasScreenshot && !hasPage) {
|
|
422
|
+
recommendations.push({
|
|
423
|
+
method: 'vllm',
|
|
424
|
+
reason: 'Only has screenshot - use VLLM validator (semantic evaluation)',
|
|
425
|
+
speed: 'slow',
|
|
426
|
+
cost: 'api',
|
|
427
|
+
reliability: 'medium'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
} else if (type === 'visual') {
|
|
431
|
+
recommendations.push({
|
|
432
|
+
method: 'vllm',
|
|
433
|
+
reason: 'Visual validation requires semantic evaluation - use VLLM',
|
|
434
|
+
speed: 'slow',
|
|
435
|
+
cost: 'api',
|
|
436
|
+
reliability: 'medium'
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
hasPage,
|
|
442
|
+
hasScreenshot,
|
|
443
|
+
recommendations,
|
|
444
|
+
recommended: recommendations[0] || null
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for Natural Language Specs
|
|
3
|
+
*
|
|
4
|
+
* Makes spec parsing, execution, and error analysis configurable per project/test.
|
|
5
|
+
* Follows the library's config pattern (like createConfig in config.mjs).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getConfig } from './config.mjs';
|
|
9
|
+
import { log } from './logger.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default spec configuration
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_SPEC_CONFIG = {
|
|
15
|
+
// Context extraction
|
|
16
|
+
useLLM: true,
|
|
17
|
+
fallback: 'regex',
|
|
18
|
+
provider: null, // Auto-detect from config
|
|
19
|
+
|
|
20
|
+
// Spec validation
|
|
21
|
+
validateBeforeExecute: true,
|
|
22
|
+
strictValidation: false, // If true, throw on validation errors
|
|
23
|
+
|
|
24
|
+
// Error analysis
|
|
25
|
+
enableErrorAnalysis: true,
|
|
26
|
+
errorAnalysisOptions: {
|
|
27
|
+
saveReport: true,
|
|
28
|
+
outputPath: null
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Template system
|
|
32
|
+
templateDir: null, // Custom template directory
|
|
33
|
+
autoLoadTemplates: true,
|
|
34
|
+
|
|
35
|
+
// Execution
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
retryOnFailure: false,
|
|
38
|
+
maxRetries: 3
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create spec configuration
|
|
43
|
+
*
|
|
44
|
+
* Merges with global config and environment variables.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} [options={}] - Configuration options
|
|
47
|
+
* @returns {Object} Spec configuration
|
|
48
|
+
*/
|
|
49
|
+
export function createSpecConfig(options = {}) {
|
|
50
|
+
const globalConfig = getConfig();
|
|
51
|
+
|
|
52
|
+
// Merge with defaults
|
|
53
|
+
const config = {
|
|
54
|
+
...DEFAULT_SPEC_CONFIG,
|
|
55
|
+
...options
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Auto-detect provider from global config if not specified
|
|
59
|
+
if (!config.provider && globalConfig.provider) {
|
|
60
|
+
config.provider = globalConfig.provider;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Respect environment variables
|
|
64
|
+
if (process.env.SPEC_USE_LLM !== undefined) {
|
|
65
|
+
config.useLLM = process.env.SPEC_USE_LLM === 'true';
|
|
66
|
+
}
|
|
67
|
+
if (process.env.SPEC_VALIDATE_BEFORE_EXECUTE !== undefined) {
|
|
68
|
+
config.validateBeforeExecute = process.env.SPEC_VALIDATE_BEFORE_EXECUTE === 'true';
|
|
69
|
+
}
|
|
70
|
+
if (process.env.SPEC_STRICT_VALIDATION !== undefined) {
|
|
71
|
+
config.strictValidation = process.env.SPEC_STRICT_VALIDATION === 'true';
|
|
72
|
+
}
|
|
73
|
+
if (process.env.SPEC_TEMPLATE_DIR) {
|
|
74
|
+
config.templateDir = process.env.SPEC_TEMPLATE_DIR;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get current spec configuration (singleton)
|
|
82
|
+
*/
|
|
83
|
+
let specConfigInstance = null;
|
|
84
|
+
|
|
85
|
+
export function getSpecConfig() {
|
|
86
|
+
if (!specConfigInstance) {
|
|
87
|
+
specConfigInstance = createSpecConfig();
|
|
88
|
+
}
|
|
89
|
+
return specConfigInstance;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set spec configuration (useful for testing)
|
|
94
|
+
*/
|
|
95
|
+
export function setSpecConfig(config) {
|
|
96
|
+
specConfigInstance = config;
|
|
97
|
+
log('[SpecConfig] Configuration updated');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reset spec configuration to defaults
|
|
102
|
+
*/
|
|
103
|
+
export function resetSpecConfig() {
|
|
104
|
+
specConfigInstance = null;
|
|
105
|
+
}
|
|
106
|
+
|