@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.
Files changed (93) hide show
  1. package/.secretsignore.example +20 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CONTRIBUTING.md +63 -0
  4. package/DEPLOYMENT.md +80 -0
  5. package/LICENSE +22 -0
  6. package/README.md +142 -0
  7. package/SECURITY.md +108 -0
  8. package/api/health.js +34 -0
  9. package/api/validate.js +252 -0
  10. package/index.d.ts +1221 -0
  11. package/package.json +112 -0
  12. package/public/index.html +149 -0
  13. package/src/batch-optimizer.mjs +451 -0
  14. package/src/bias-detector.mjs +370 -0
  15. package/src/bias-mitigation.mjs +233 -0
  16. package/src/cache.mjs +433 -0
  17. package/src/config.mjs +268 -0
  18. package/src/constants.mjs +80 -0
  19. package/src/context-compressor.mjs +350 -0
  20. package/src/convenience.mjs +617 -0
  21. package/src/cost-tracker.mjs +257 -0
  22. package/src/cross-modal-consistency.mjs +170 -0
  23. package/src/data-extractor.mjs +232 -0
  24. package/src/dynamic-few-shot.mjs +140 -0
  25. package/src/dynamic-prompts.mjs +361 -0
  26. package/src/ensemble/index.mjs +53 -0
  27. package/src/ensemble-judge.mjs +366 -0
  28. package/src/error-handler.mjs +67 -0
  29. package/src/errors.mjs +167 -0
  30. package/src/experience-propagation.mjs +128 -0
  31. package/src/experience-tracer.mjs +487 -0
  32. package/src/explanation-manager.mjs +299 -0
  33. package/src/feedback-aggregator.mjs +248 -0
  34. package/src/game-goal-prompts.mjs +478 -0
  35. package/src/game-player.mjs +548 -0
  36. package/src/hallucination-detector.mjs +155 -0
  37. package/src/helpers/playwright.mjs +80 -0
  38. package/src/human-validation-manager.mjs +516 -0
  39. package/src/index.mjs +364 -0
  40. package/src/judge.mjs +929 -0
  41. package/src/latency-aware-batch-optimizer.mjs +192 -0
  42. package/src/load-env.mjs +159 -0
  43. package/src/logger.mjs +55 -0
  44. package/src/metrics.mjs +187 -0
  45. package/src/model-tier-selector.mjs +221 -0
  46. package/src/multi-modal/index.mjs +36 -0
  47. package/src/multi-modal-fusion.mjs +190 -0
  48. package/src/multi-modal.mjs +524 -0
  49. package/src/natural-language-specs.mjs +1071 -0
  50. package/src/pair-comparison.mjs +277 -0
  51. package/src/persona/index.mjs +42 -0
  52. package/src/persona-enhanced.mjs +200 -0
  53. package/src/persona-experience.mjs +572 -0
  54. package/src/position-counterbalance.mjs +140 -0
  55. package/src/prompt-composer.mjs +375 -0
  56. package/src/render-change-detector.mjs +583 -0
  57. package/src/research-enhanced-validation.mjs +436 -0
  58. package/src/retry.mjs +152 -0
  59. package/src/rubrics.mjs +231 -0
  60. package/src/score-tracker.mjs +277 -0
  61. package/src/smart-validator.mjs +447 -0
  62. package/src/spec-config.mjs +106 -0
  63. package/src/spec-templates.mjs +347 -0
  64. package/src/specs/index.mjs +38 -0
  65. package/src/temporal/index.mjs +102 -0
  66. package/src/temporal-adaptive.mjs +163 -0
  67. package/src/temporal-batch-optimizer.mjs +222 -0
  68. package/src/temporal-constants.mjs +69 -0
  69. package/src/temporal-context.mjs +49 -0
  70. package/src/temporal-decision-manager.mjs +271 -0
  71. package/src/temporal-decision.mjs +669 -0
  72. package/src/temporal-errors.mjs +58 -0
  73. package/src/temporal-note-pruner.mjs +173 -0
  74. package/src/temporal-preprocessor.mjs +543 -0
  75. package/src/temporal-prompt-formatter.mjs +219 -0
  76. package/src/temporal-validation.mjs +159 -0
  77. package/src/temporal.mjs +415 -0
  78. package/src/type-guards.mjs +311 -0
  79. package/src/uncertainty-reducer.mjs +470 -0
  80. package/src/utils/index.mjs +175 -0
  81. package/src/validation-framework.mjs +321 -0
  82. package/src/validation-result-normalizer.mjs +64 -0
  83. package/src/validation.mjs +243 -0
  84. package/src/validators/accessibility-programmatic.mjs +345 -0
  85. package/src/validators/accessibility-validator.mjs +223 -0
  86. package/src/validators/batch-validator.mjs +143 -0
  87. package/src/validators/hybrid-validator.mjs +268 -0
  88. package/src/validators/index.mjs +34 -0
  89. package/src/validators/prompt-builder.mjs +218 -0
  90. package/src/validators/rubric.mjs +85 -0
  91. package/src/validators/state-programmatic.mjs +260 -0
  92. package/src/validators/state-validator.mjs +291 -0
  93. package/vercel.json +27 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Ensemble Judging
3
+ *
4
+ * Implements multiple LLM judges with consensus voting.
5
+ * Research shows ensemble judging improves accuracy and reduces bias.
6
+ *
7
+ * Supports:
8
+ * - Multiple judges (different providers or prompts)
9
+ * - Weighted voting
10
+ * - Consensus calculation
11
+ * - Disagreement analysis
12
+ */
13
+
14
+ import { VLLMJudge } from './judge.mjs';
15
+ import { detectBias, detectPositionBias } from './bias-detector.mjs';
16
+
17
+ /**
18
+ * Ensemble Judge Class
19
+ *
20
+ * Manages multiple judges and aggregates their results.
21
+ *
22
+ * @class EnsembleJudge
23
+ */
24
+ export class EnsembleJudge {
25
+ /**
26
+ * @param {import('./index.mjs').EnsembleJudgeOptions} [options={}] - Ensemble configuration
27
+ */
28
+ constructor(options = {}) {
29
+ const {
30
+ judges = [],
31
+ votingMethod = 'weighted_average', // 'weighted_average', 'majority', 'consensus', 'optimal'
32
+ weights = null, // Array of weights for each judge
33
+ judgeAccuracies = null, // Array of accuracy scores (0-1) for optimal weighting
34
+ minAgreement = 0.7, // Minimum agreement for consensus
35
+ enableBiasDetection = true
36
+ } = options;
37
+
38
+ this.judges = judges.length > 0 ? judges : [new VLLMJudge()];
39
+ this.votingMethod = votingMethod;
40
+ this.judgeAccuracies = judgeAccuracies; // For optimal weighting (arXiv:2510.01499)
41
+ this.weights = weights || this.judges.map(() => 1.0);
42
+ this.minAgreement = minAgreement;
43
+ this.enableBiasDetection = enableBiasDetection;
44
+
45
+ // Calculate weights based on method
46
+ if (votingMethod === 'optimal' && this.judgeAccuracies) {
47
+ this.weights = this.calculateOptimalWeights(this.judgeAccuracies);
48
+ }
49
+
50
+ // Normalize weights
51
+ const weightSum = this.weights.reduce((a, b) => a + b, 0);
52
+ this.normalizedWeights = this.weights.map(w => w / weightSum);
53
+ }
54
+
55
+ /**
56
+ * Calculate optimal weights using inverse generalized sigmoid function
57
+ * Research: arXiv:2510.01499 - ω_i = σ_K^{-1}(x_i) where σ_K(x) = e^x/(K-1+e^x)
58
+ *
59
+ * CORRECTED: Uses generalized sigmoid σ_K(x) for N models, not standard logistic σ(x)
60
+ * For K=2 models, this reduces to standard logistic. For K>2, the formula differs.
61
+ *
62
+ * @param {number[]} accuracies - Array of accuracy scores (0-1) for each judge
63
+ * @returns {number[]} Optimal weights
64
+ */
65
+ calculateOptimalWeights(accuracies) {
66
+ const K = accuracies.length; // Number of models
67
+
68
+ // Edge case: single judge gets weight 1.0
69
+ if (K === 1) {
70
+ return [1.0];
71
+ }
72
+
73
+ // Handle edge cases: p=0 → -∞, p=1 → +∞, so clamp to [0.001, 0.999]
74
+ const clamped = accuracies.map(a => Math.max(0.001, Math.min(0.999, a)));
75
+
76
+ // CORRECT formula: σ_K^{-1}(x) = ln(x(K-1) / (1-x))
77
+ // This is the inverse of σ_K(x) = e^x/(K-1+e^x)
78
+ const inverseSigmoid = clamped.map(p => {
79
+ if (p <= 0 || p >= 1) return 0; // Safety check
80
+ const numerator = p * (K - 1);
81
+ const denominator = 1 - p;
82
+ if (denominator <= 0 || numerator <= 0) return 0; // Safety check (handles K=1 case)
83
+ const ratio = numerator / denominator;
84
+ if (ratio <= 0) return 0; // Safety check for ln(0) or ln(negative)
85
+ return Math.log(ratio);
86
+ });
87
+
88
+ // Normalize to positive weights (shift by min to make all positive, preserve ratios)
89
+ const min = Math.min(...inverseSigmoid);
90
+ const shifted = inverseSigmoid.map(w => {
91
+ const shiftedValue = w - min + 1;
92
+ // Ensure positive weight (clamp to minimum 0.001 to avoid zero weights)
93
+ return Math.max(0.001, shiftedValue);
94
+ });
95
+
96
+ return shifted;
97
+ }
98
+
99
+ /**
100
+ * Evaluate screenshot with ensemble of judges
101
+ *
102
+ * @param {string} imagePath - Path to screenshot file
103
+ * @param {string} prompt - Evaluation prompt
104
+ * @param {import('./index.mjs').ValidationContext} [context={}] - Validation context
105
+ * @returns {Promise<import('./index.mjs').EnsembleResult>} Ensemble evaluation result
106
+ */
107
+ async evaluate(imagePath, prompt, context = {}) {
108
+ // Run all judges in parallel
109
+ const judgments = await Promise.all(
110
+ this.judges.map((judge, index) =>
111
+ judge.judgeScreenshot(imagePath, prompt, {
112
+ ...context,
113
+ judgeIndex: index,
114
+ judgeCount: this.judges.length
115
+ }).catch(error => ({
116
+ error: error.message,
117
+ judgeIndex: index,
118
+ score: null
119
+ }))
120
+ )
121
+ );
122
+
123
+ // Extract scores and results
124
+ const results = judgments.map((judgment, index) => ({
125
+ judgeIndex: index,
126
+ score: judgment.score,
127
+ assessment: judgment.assessment,
128
+ issues: judgment.issues || [],
129
+ reasoning: judgment.reasoning,
130
+ provider: judgment.provider,
131
+ error: judgment.error,
132
+ raw: judgment
133
+ }));
134
+
135
+ // Aggregate results
136
+ const aggregated = this.aggregateResults(results);
137
+
138
+ // Detect biases if enabled
139
+ if (this.enableBiasDetection) {
140
+ aggregated.biasDetection = {
141
+ individual: results.map(r => detectBias(r.reasoning || '')),
142
+ position: detectPositionBias(results)
143
+ };
144
+ }
145
+
146
+ // Calculate agreement
147
+ aggregated.agreement = this.calculateAgreement(results);
148
+ aggregated.disagreement = this.analyzeDisagreement(results);
149
+
150
+ return {
151
+ ...aggregated,
152
+ individualJudgments: results,
153
+ judgeCount: this.judges.length,
154
+ votingMethod: this.votingMethod
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Aggregate results based on voting method
160
+ */
161
+ aggregateResults(results) {
162
+ const validResults = results.filter(r => r.score !== null && !r.error);
163
+
164
+ if (validResults.length === 0) {
165
+ return {
166
+ score: null,
167
+ assessment: 'error',
168
+ issues: ['All judges failed'],
169
+ reasoning: 'All judges encountered errors',
170
+ confidence: 0
171
+ };
172
+ }
173
+
174
+ switch (this.votingMethod) {
175
+ case 'weighted_average':
176
+ case 'optimal':
177
+ return this.weightedAverage(validResults);
178
+ case 'majority':
179
+ return this.majorityVote(validResults);
180
+ case 'consensus':
181
+ return this.consensusVote(validResults);
182
+ default:
183
+ return this.weightedAverage(validResults);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Weighted average voting
189
+ */
190
+ weightedAverage(results) {
191
+ const scores = results.map((r, i) => ({
192
+ score: r.score,
193
+ weight: this.normalizedWeights[r.judgeIndex] || 1.0 / results.length
194
+ }));
195
+
196
+ const weightedSum = scores.reduce((sum, s) => sum + (s.score * s.weight), 0);
197
+ const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
198
+ const avgScore = totalWeight > 0 ? weightedSum / totalWeight : null;
199
+
200
+ // Aggregate issues (union)
201
+ const allIssues = new Set();
202
+ results.forEach(r => {
203
+ if (r.issues) r.issues.forEach(issue => allIssues.add(issue));
204
+ });
205
+
206
+ // Aggregate reasoning
207
+ const reasoning = results
208
+ .map((r, i) => `Judge ${i + 1} (${r.provider}): ${r.reasoning || 'No reasoning'}`)
209
+ .join('\n\n');
210
+
211
+ // Determine assessment
212
+ const assessment = avgScore >= 7 ? 'pass' : avgScore >= 5 ? 'needs-improvement' : 'fail';
213
+
214
+ return {
215
+ score: Math.round(avgScore * 10) / 10, // Round to 1 decimal
216
+ assessment,
217
+ issues: Array.from(allIssues),
218
+ reasoning: `Ensemble judgment (weighted average):\n${reasoning}`,
219
+ confidence: this.calculateConfidence(results, avgScore)
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Majority vote
225
+ */
226
+ majorityVote(results) {
227
+ const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : r.score >= 5 ? 'needs-improvement' : 'fail'));
228
+ const assessmentCounts = {};
229
+ assessments.forEach(a => {
230
+ assessmentCounts[a] = (assessmentCounts[a] || 0) + 1;
231
+ });
232
+
233
+ const majorityAssessment = Object.entries(assessmentCounts)
234
+ .sort((a, b) => b[1] - a[1])[0][0];
235
+
236
+ // Average score of majority
237
+ const majorityResults = results.filter((r, i) => assessments[i] === majorityAssessment);
238
+ const avgScore = majorityResults.reduce((sum, r) => sum + r.score, 0) / majorityResults.length;
239
+
240
+ return {
241
+ score: Math.round(avgScore * 10) / 10,
242
+ assessment: majorityAssessment,
243
+ issues: Array.from(new Set(majorityResults.flatMap(r => r.issues || []))),
244
+ reasoning: `Majority vote: ${majorityAssessment} (${assessmentCounts[majorityAssessment]}/${results.length} judges)`,
245
+ confidence: assessmentCounts[majorityAssessment] / results.length
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Consensus vote (requires high agreement)
251
+ */
252
+ consensusVote(results) {
253
+ const agreement = this.calculateAgreement(results);
254
+
255
+ if (agreement.score < this.minAgreement) {
256
+ // No consensus - return weighted average with low confidence
257
+ const avg = this.weightedAverage(results);
258
+ return {
259
+ ...avg,
260
+ assessment: 'no-consensus',
261
+ confidence: agreement.score,
262
+ reasoning: `No consensus reached (agreement: ${(agreement.score * 100).toFixed(0)}%). ${avg.reasoning}`
263
+ };
264
+ }
265
+
266
+ // Consensus reached - return weighted average
267
+ return this.weightedAverage(results);
268
+ }
269
+
270
+ /**
271
+ * Calculate agreement between judges
272
+ */
273
+ calculateAgreement(results) {
274
+ if (results.length < 2) {
275
+ return { score: 1.0, type: 'single_judge' };
276
+ }
277
+
278
+ const scores = results.map(r => r.score).filter(s => s !== null);
279
+ if (scores.length < 2) {
280
+ return { score: 0, type: 'insufficient_scores' };
281
+ }
282
+
283
+ // Calculate variance
284
+ const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
285
+ const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
286
+ const stdDev = Math.sqrt(variance);
287
+
288
+ // Agreement is inverse of normalized standard deviation
289
+ // Max std dev for 0-10 scale is ~5, so normalize
290
+ const normalizedStdDev = stdDev / 5;
291
+ const agreement = Math.max(0, 1 - normalizedStdDev);
292
+
293
+ // Check assessment agreement
294
+ const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : 'fail'));
295
+ const uniqueAssessments = new Set(assessments);
296
+ const assessmentAgreement = uniqueAssessments.size === 1 ? 1.0 : 0.5;
297
+
298
+ return {
299
+ score: (agreement + assessmentAgreement) / 2,
300
+ scoreAgreement: agreement,
301
+ assessmentAgreement,
302
+ mean,
303
+ stdDev,
304
+ scores
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Analyze disagreement between judges
310
+ */
311
+ analyzeDisagreement(results) {
312
+ if (results.length < 2) {
313
+ return { hasDisagreement: false };
314
+ }
315
+
316
+ const scores = results.map(r => r.score).filter(s => s !== null);
317
+ const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : 'fail'));
318
+
319
+ const scoreRange = Math.max(...scores) - Math.min(...scores);
320
+ const uniqueAssessments = new Set(assessments);
321
+
322
+ return {
323
+ hasDisagreement: scoreRange > 2 || uniqueAssessments.size > 1,
324
+ scoreRange,
325
+ assessmentDisagreement: uniqueAssessments.size > 1,
326
+ uniqueAssessments: Array.from(uniqueAssessments),
327
+ maxScore: Math.max(...scores),
328
+ minScore: Math.min(...scores)
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Calculate confidence in aggregated result
334
+ */
335
+ calculateConfidence(results, avgScore) {
336
+ const agreement = this.calculateAgreement(results);
337
+ const disagreement = this.analyzeDisagreement(results);
338
+
339
+ // Confidence based on agreement and number of judges
340
+ const agreementConfidence = agreement.score;
341
+ const judgeCountConfidence = Math.min(1.0, results.length / 3); // More judges = more confidence
342
+ const disagreementPenalty = disagreement.hasDisagreement ? 0.2 : 0;
343
+
344
+ return Math.max(0, Math.min(1.0, (agreementConfidence * 0.7 + judgeCountConfidence * 0.3) - disagreementPenalty));
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Create an ensemble judge with multiple providers
350
+ *
351
+ * @param {string[]} [providers=['gemini', 'openai']] - Array of provider names
352
+ * @param {import('./index.mjs').EnsembleJudgeOptions} [options={}] - Ensemble configuration
353
+ * @returns {EnsembleJudge} Configured ensemble judge
354
+ */
355
+ export function createEnsembleJudge(providers = ['gemini', 'openai'], options = {}) {
356
+ const judges = providers.map(provider => {
357
+ const judge = new VLLMJudge({ provider });
358
+ return judge;
359
+ });
360
+
361
+ return new EnsembleJudge({
362
+ ...options,
363
+ judges
364
+ });
365
+ }
366
+
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Global Error Handler
3
+ *
4
+ * Handles unhandled promise rejections and uncaught exceptions.
5
+ * Prevents silent failures and improves debugging.
6
+ */
7
+
8
+ import { error } from './logger.mjs';
9
+
10
+ /**
11
+ * Initialize global error handlers
12
+ *
13
+ * **Opt-in**: This function is exported but not automatically called.
14
+ * Users must explicitly call `initErrorHandlers()` if they want global
15
+ * error handling for unhandled rejections and uncaught exceptions.
16
+ *
17
+ * Should be called early in application startup.
18
+ * Only call once per process.
19
+ *
20
+ * @example
21
+ * ```javascript
22
+ * import { initErrorHandlers } from 'ai-visual-test';
23
+ * initErrorHandlers(); // Opt-in to global error handling
24
+ * ```
25
+ */
26
+ export function initErrorHandlers() {
27
+ // Handle unhandled promise rejections
28
+ process.on('unhandledRejection', (reason, promise) => {
29
+ error('[Unhandled Rejection]', {
30
+ reason: reason instanceof Error ? {
31
+ message: reason.message,
32
+ stack: reason.stack,
33
+ name: reason.name
34
+ } : reason,
35
+ promise: promise?.toString?.() || 'Unknown promise'
36
+ });
37
+
38
+ // In production, you might want to:
39
+ // - Log to monitoring service (Sentry, DataDog, etc.)
40
+ // - Send alerts
41
+ // - Gracefully shutdown
42
+ });
43
+
44
+ // Handle uncaught exceptions
45
+ process.on('uncaughtException', (err) => {
46
+ error('[Uncaught Exception]', {
47
+ message: err.message,
48
+ stack: err.stack,
49
+ name: err.name
50
+ });
51
+
52
+ // NOTE: Libraries should not call process.exit()
53
+ // Let the application decide how to handle uncaught exceptions.
54
+ // Users can add their own process.exit(1) if needed, or use a process manager
55
+ // that handles restarts automatically.
56
+ });
57
+
58
+ // Handle warnings
59
+ process.on('warning', (warning) => {
60
+ error('[Process Warning]', {
61
+ name: warning.name,
62
+ message: warning.message,
63
+ stack: warning.stack
64
+ });
65
+ });
66
+ }
67
+
package/src/errors.mjs ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Custom Error Classes for ai-visual-test
3
+ *
4
+ * Provides standardized error handling across the package.
5
+ * Based on Playwright's error handling patterns and industry best practices.
6
+ *
7
+ * All errors extend AIBrowserTestError for consistent error handling and serialization.
8
+ */
9
+
10
+ /**
11
+ * Base error class for all ai-visual-test errors
12
+ *
13
+ * @class AIBrowserTestError
14
+ * @extends {Error}
15
+ */
16
+ export class AIBrowserTestError extends Error {
17
+ /**
18
+ * @param {string} message - Error message
19
+ * @param {string} code - Error code
20
+ * @param {Record<string, unknown>} [details={}] - Additional error details
21
+ */
22
+ constructor(message, code, details = {}) {
23
+ super(message);
24
+ this.name = this.constructor.name;
25
+ this.code = code;
26
+ this.details = details;
27
+
28
+ // Maintains proper stack trace for where error was thrown (V8 only)
29
+ if (Error.captureStackTrace) {
30
+ Error.captureStackTrace(this, this.constructor);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Convert error to JSON for serialization
36
+ *
37
+ * @returns {import('./index.mjs').AIBrowserTestError['toJSON']} JSON representation
38
+ */
39
+ toJSON() {
40
+ return {
41
+ name: this.name,
42
+ code: this.code,
43
+ message: this.message,
44
+ details: this.details,
45
+ stack: this.stack
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validation error - thrown when validation fails
52
+ *
53
+ * @class ValidationError
54
+ * @extends {AIBrowserTestError}
55
+ */
56
+ export class ValidationError extends AIBrowserTestError {
57
+ /**
58
+ * @param {string} message - Error message
59
+ * @param {Record<string, unknown>} [details={}] - Additional error details
60
+ */
61
+ constructor(message, details = {}) {
62
+ super(message, 'VALIDATION_ERROR', details);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Cache error - thrown when cache operations fail
68
+ */
69
+ export class CacheError extends AIBrowserTestError {
70
+ constructor(message, details = {}) {
71
+ super(message, 'CACHE_ERROR', details);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Config error - thrown when configuration is invalid
77
+ */
78
+ export class ConfigError extends AIBrowserTestError {
79
+ constructor(message, details = {}) {
80
+ super(message, 'CONFIG_ERROR', details);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Provider error - thrown when VLLM provider operations fail
86
+ */
87
+ export class ProviderError extends AIBrowserTestError {
88
+ constructor(message, provider, details = {}) {
89
+ super(message, 'PROVIDER_ERROR', { provider, ...details });
90
+ this.provider = provider;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Timeout error - thrown when operations timeout
96
+ */
97
+ export class TimeoutError extends AIBrowserTestError {
98
+ constructor(message, timeout, details = {}) {
99
+ super(message, 'TIMEOUT_ERROR', { timeout, ...details });
100
+ this.timeout = timeout;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * File error - thrown when file operations fail
106
+ */
107
+ export class FileError extends AIBrowserTestError {
108
+ constructor(message, filePath, details = {}) {
109
+ super(message, 'FILE_ERROR', { filePath, ...details });
110
+ this.filePath = filePath;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * State mismatch error - thrown when state validation fails
116
+ *
117
+ * @class StateMismatchError
118
+ * @extends {ValidationError}
119
+ */
120
+ export class StateMismatchError extends ValidationError {
121
+ /**
122
+ * @param {string[]} discrepancies - List of discrepancies found
123
+ * @param {unknown} extracted - Extracted state
124
+ * @param {unknown} expected - Expected state
125
+ * @param {string} [message] - Custom error message
126
+ */
127
+ constructor(discrepancies, extracted, expected, message) {
128
+ const defaultMessage = `State mismatch: ${discrepancies.length} discrepancy(ies) found`;
129
+ super(
130
+ message || defaultMessage,
131
+ {
132
+ discrepancies,
133
+ extracted,
134
+ expected,
135
+ discrepancyCount: discrepancies.length
136
+ }
137
+ );
138
+ this.discrepancies = discrepancies;
139
+ this.extracted = extracted;
140
+ this.expected = expected;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check if error is an instance of AIBrowserTestError
146
+ *
147
+ * @param {unknown} error - Error to check
148
+ * @returns {error is AIBrowserTestError} True if error is an AIBrowserTestError
149
+ */
150
+ export function isAIBrowserTestError(error) {
151
+ return error instanceof AIBrowserTestError;
152
+ }
153
+
154
+ /**
155
+ * Check if error is a specific error type
156
+ *
157
+ * @template {new (...args: any[]) => AIBrowserTestError} T
158
+ * @param {unknown} error - Error to check
159
+ * @param {T} errorClass - Error class constructor
160
+ * @returns {error is InstanceType<T>} True if error is instance of errorClass
161
+ */
162
+ export function isErrorType(error, errorClass) {
163
+ return error instanceof errorClass;
164
+ }
165
+
166
+
167
+