qa360 2.2.12 → 2.2.13

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.
@@ -32,16 +32,28 @@ export class OllamaProvider {
32
32
  }
33
33
  async isAvailable() {
34
34
  try {
35
- // Use timeout with AbortSignal - wrap for compatibility
35
+ // Use a longer timeout (15 seconds) to accommodate slower systems
36
+ // Ollama can take time to respond, especially on first run or with many models
36
37
  const controller = new AbortController();
37
- const timeoutId = setTimeout(() => controller.abort(), 5000);
38
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
38
39
  const response = await fetch(`${this.baseUrl}/api/tags`, {
39
40
  signal: controller.signal,
40
41
  });
41
42
  clearTimeout(timeoutId);
42
43
  return response.ok;
43
44
  }
44
- catch {
45
+ catch (error) {
46
+ // Log the error for debugging (will be visible in verbose mode)
47
+ if (error instanceof Error) {
48
+ if (error.name === 'AbortError') {
49
+ // Timeout - Ollama is slow to respond
50
+ console.debug(`[Ollama] Connection to ${this.baseUrl} timed out after 15s`);
51
+ }
52
+ else {
53
+ // Other error (ECONNREFUSED, ENOTFOUND, etc.)
54
+ console.debug(`[Ollama] Connection error: ${error.message}`);
55
+ }
56
+ }
45
57
  return false;
46
58
  }
47
59
  }
@@ -50,9 +50,12 @@ export function generateSelector(element) {
50
50
  if (className && !isGenericClass(className)) {
51
51
  const classes = className.split(' ').filter(c => !isGenericClass(c));
52
52
  if (classes.length > 0) {
53
- const classSelector = classes.map(c => `.${escapeCss(c)}`).join('');
54
- // Qualify with tag if available
55
- return tagName ? `${tagName}${classSelector}` : classSelector;
53
+ // Smart class selection: prioritize meaningful classes
54
+ const meaningfulClasses = selectMeaningfulClasses(classes);
55
+ const classSelector = meaningfulClasses.map(c => `.${escapeCss(c)}`).join('');
56
+ // Limit total selector length
57
+ const qualifiedSelector = tagName ? `${tagName}${classSelector}` : classSelector;
58
+ return truncateSelector(qualifiedSelector, 200);
56
59
  }
57
60
  }
58
61
  // 7. Tag + text content (for buttons, links)
@@ -169,6 +172,72 @@ function isSelectorStable(selector) {
169
172
  function escapeCss(str) {
170
173
  return str.replace(/(["\\])/g, '\\$1').replace(/"/g, '\\"');
171
174
  }
175
+ /**
176
+ * Select the most meaningful classes from a class list
177
+ * Prioritizes: unique identifiers, component names, action-oriented classes
178
+ * Deprioritizes: utility classes (spacing, colors, sizing)
179
+ */
180
+ function selectMeaningfulClasses(classes) {
181
+ // Tailwind and other utility class patterns to deprioritize
182
+ const utilityPatterns = [
183
+ /^(p|m|px|py|pt|pb|pl|pr)-/, // spacing
184
+ /^(mt|mb|ml|mr)-/, // margin spacing
185
+ /^(text|bg|border)-/, // colors
186
+ /^(w|h)-/, // sizing
187
+ /^(flex|grid|block|inline)/, // display
188
+ /^(justify|items|self)/, // flexbox/grid
189
+ /^(rounded|shadow|opacity)/, // styling
190
+ /^gap-/, // flex gap
191
+ /^(line-)?height-/, // height
192
+ /^(font|weight|size)/, // typography
193
+ ];
194
+ // Score each class by "meaningfulness"
195
+ const scored = classes.map(cls => {
196
+ let score = 50; // base score
197
+ // Bonus: component-like names (contain meaningful words)
198
+ if (/[A-Z][a-z]+/.test(cls))
199
+ score += 30; // CamelCase components
200
+ if (/button|btn|input|form|card|modal|nav|header|footer|sidebar|menu|dropdown/i.test(cls))
201
+ score += 25;
202
+ // Bonus: contains numbers (likely unique)
203
+ if (/\d+/.test(cls))
204
+ score += 15;
205
+ // Penalty: utility classes
206
+ for (const pattern of utilityPatterns) {
207
+ if (pattern.test(cls)) {
208
+ score -= 40;
209
+ break;
210
+ }
211
+ }
212
+ return { class: cls, score };
213
+ });
214
+ // Sort by score (descending) and take top 5
215
+ scored.sort((a, b) => b.score - a.score);
216
+ return scored.slice(0, 5).map(s => s.class);
217
+ }
218
+ /**
219
+ * Truncate selector to max length while keeping it valid
220
+ * Preserves the most specific parts
221
+ */
222
+ function truncateSelector(selector, maxLength) {
223
+ if (selector.length <= maxLength)
224
+ return selector;
225
+ // For class-based selectors, remove classes from the middle
226
+ const classMatch = selector.match(/^(\w+)((\.[\w-]+)+)$/);
227
+ if (classMatch) {
228
+ const tag = classMatch[1];
229
+ const classes = classMatch[2].split('.').filter(c => c); // Remove empty strings
230
+ // Keep first 2 and last 2 classes (most specific)
231
+ if (classes.length > 4) {
232
+ const kept = [...classes.slice(0, 2), ...classes.slice(-2)];
233
+ const truncated = `${tag}.${kept.join('.')}`;
234
+ if (truncated.length <= maxLength)
235
+ return truncated;
236
+ }
237
+ }
238
+ // Final fallback: just truncate (may not be valid CSS but prevents overflow)
239
+ return selector.substring(0, maxLength - 3) + '...';
240
+ }
172
241
  /**
173
242
  * Optimize selector for resiliency
174
243
  */
@@ -191,6 +191,20 @@ function generateFormTestSteps(form) {
191
191
  }
192
192
  return steps;
193
193
  }
194
+ /**
195
+ * Escape a string value for YAML output
196
+ * Handles quotes, backslashes, and special characters
197
+ */
198
+ function escapeYamlString(value) {
199
+ if (!value)
200
+ return '';
201
+ return value
202
+ .replace(/\\/g, '\\\\') // Backslashes first
203
+ .replace(/"/g, '\\"') // Double quotes
204
+ .replace(/\n/g, '\\n') // Newlines
205
+ .replace(/\r/g, '\\r') // Carriage returns
206
+ .replace(/\t/g, '\\t'); // Tabs
207
+ }
194
208
  /**
195
209
  * Generate YAML for a UI test definition
196
210
  */
@@ -198,20 +212,20 @@ function generateUiTestYaml(test) {
198
212
  const steps = test.steps.map(step => {
199
213
  let yaml = ` - action: ${step.action}`;
200
214
  if (step.description)
201
- yaml += `\n description: "${step.description}"`;
215
+ yaml += `\n description: "${escapeYamlString(step.description)}"`;
202
216
  if (step.selector)
203
- yaml += `\n selector: "${step.selector}"`;
217
+ yaml += `\n selector: "${escapeYamlString(step.selector)}"`;
204
218
  if (step.value)
205
- yaml += `\n value: "${step.value}"`;
219
+ yaml += `\n value: "${escapeYamlString(String(step.value))}"`;
206
220
  if (step.expected)
207
221
  yaml += `\n expected: ${JSON.stringify(step.expected)}`;
208
222
  if (step.wait)
209
223
  yaml += `\n wait: ${step.wait}`;
210
224
  return yaml;
211
225
  }).join('\n');
212
- return ` - name: "${test.name}"
213
- description: "${test.description || ''}"
214
- path: "${test.path || '/'}"
226
+ return ` - name: "${escapeYamlString(test.name)}"
227
+ description: "${escapeYamlString(test.description || '')}"
228
+ path: "${escapeYamlString(test.path || '/')}"
215
229
  steps:
216
230
  ${steps}`;
217
231
  }
@@ -22,8 +22,8 @@ export class PackValidator {
22
22
  const __dirname = dirname(__filename);
23
23
  // Check multiple paths to handle both development and bundled npm environments
24
24
  // From cli/dist/core/pack/validator.js, we need to reach core/schemas/pack.schema.json
25
- const path1 = join(__dirname, '../../schemas/pack.schema.json'); // bundled (cli/dist/core/schemas/)
26
- const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json'); // npm package (core/schemas/)
25
+ const path1 = join(__dirname, '../../schemas/pack.schema.json'); // bundled (cli/dist/core/schemas/)
26
+ const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json'); // npm package (core/schemas/)
27
27
  let schemaPath = existsSync(path1) ? path1 : path2;
28
28
  // Final fallback: throw error if no schema found
29
29
  if (!existsSync(schemaPath)) {
@@ -47,6 +47,7 @@ export declare class PackMigrator {
47
47
  private findSecretInEnv;
48
48
  /**
49
49
  * Sanitize gate name for v2 format
50
+ * Handles both strings and objects (from YAML parsing edge cases)
50
51
  */
51
52
  private sanitizeGateName;
52
53
  /**
@@ -424,8 +424,33 @@ export class PackMigrator {
424
424
  }
425
425
  /**
426
426
  * Sanitize gate name for v2 format
427
+ * Handles both strings and objects (from YAML parsing edge cases)
427
428
  */
428
429
  sanitizeGateName(gate) {
430
+ // Handle edge case where gate might be an object from YAML parsing
431
+ if (typeof gate !== 'string') {
432
+ // If gate is an object, try to extract a name
433
+ if (gate && typeof gate === 'object') {
434
+ // Try common property names
435
+ const possibleName = gate.name || gate.id || gate.gate;
436
+ if (typeof possibleName === 'string') {
437
+ gate = possibleName;
438
+ }
439
+ else {
440
+ // Fallback: stringify the object keys
441
+ const keys = Object.keys(gate);
442
+ if (keys.length === 1) {
443
+ gate = keys[0];
444
+ }
445
+ else {
446
+ throw new Error(`Invalid gate format: expected string, got object with keys: ${keys.join(', ')}`);
447
+ }
448
+ }
449
+ }
450
+ else {
451
+ throw new Error(`Invalid gate format: expected string, got ${typeof gate}`);
452
+ }
453
+ }
429
454
  // Map v1 gate names to v2 conventions
430
455
  const nameMap = {
431
456
  'api_smoke': 'smoke',
@@ -370,7 +370,8 @@ export class PackValidatorV2 {
370
370
  // Validate auth profile reference
371
371
  if (gate.auth) {
372
372
  // Will be validated against profiles in auth section
373
- if (!gate.adapter?.includes('api') && !gate.adapter?.includes('ui')) {
373
+ const adapter = typeof gate.adapter === 'string' ? gate.adapter : '';
374
+ if (adapter && !adapter.includes('api') && !adapter.includes('ui')) {
374
375
  warnings.push({
375
376
  code: 'QP2V019',
376
377
  message: `Auth profile specified but adapter may not support auth`,
@@ -593,7 +594,7 @@ export class PackValidatorV2 {
593
594
  return { errors, warnings };
594
595
  }
595
596
  // Check for API gate with api auth but no auth profile
596
- const apiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('api'));
597
+ const apiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('api'));
597
598
  if (apiGates.length > 0 && !pack.auth?.api && !pack.auth?.profiles) {
598
599
  warnings.push({
599
600
  code: 'QP2V034',
@@ -603,7 +604,7 @@ export class PackValidatorV2 {
603
604
  });
604
605
  }
605
606
  // Check for UI gate with ui auth but no auth profile
606
- const uiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('ui'));
607
+ const uiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('ui'));
607
608
  if (uiGates.length > 0 && !pack.auth?.ui && !pack.auth?.profiles) {
608
609
  warnings.push({
609
610
  code: 'QP2V035',
@@ -881,10 +881,21 @@ export class Phase3Runner {
881
881
  * Run UI gate (includes basic accessibility)
882
882
  */
883
883
  async runUiGate() {
884
- const target = this.getTargetWeb();
884
+ let target = this.getTargetWeb();
885
885
  if (!target) {
886
886
  throw new Error('UI gate requires targets.web configuration or gate config with baseUrl');
887
887
  }
888
+ // Transform relative pages to full URLs for v1 packs
889
+ if (target.pages && target.baseUrl && !this.isPackV2(this.pack)) {
890
+ const baseUrl = target.baseUrl.replace(/\/$/, '');
891
+ target.pages = target.pages.map((p) => {
892
+ const pageStr = typeof p === 'object' && p.url ? p.url : p;
893
+ if (typeof pageStr === 'string' && !pageStr.startsWith('http')) {
894
+ return `${baseUrl}${pageStr.startsWith('/') ? '' : '/'}${pageStr}`;
895
+ }
896
+ return p;
897
+ });
898
+ }
888
899
  const credentials = await this.getCredentialsForGate('ui');
889
900
  const adapter = new PlaywrightUiAdapter();
890
901
  return await adapter.runSmokeTests({
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.12",
3
+ "version": "2.2.13",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.12",
3
+ "version": "2.2.13",
4
4
  "description": "Transform software testing into verifiable, signed, and traceable proofs",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.2",
@@ -1,101 +0,0 @@
1
- /**
2
- * Coverage Analyzer
3
- *
4
- * Analyzes coverage data to provide insights, trends, and recommendations.
5
- */
6
- import type { FileCoverage, CoverageMetrics, CoverageResult, CoverageTrend, CoverageGap, CoverageComparison, CoverageThreshold, CoverageType, CoverageReport } from './types.js';
7
- /**
8
- * Historical coverage data point
9
- */
10
- interface HistoricalCoverage {
11
- runId: string;
12
- timestamp: number;
13
- metrics: CoverageMetrics;
14
- }
15
- /**
16
- * Coverage Analyzer class
17
- */
18
- export declare class CoverageAnalyzer {
19
- private history;
20
- /**
21
- * Analyze coverage and generate insights
22
- */
23
- analyze(result: CoverageResult, threshold?: CoverageThreshold): CoverageReport;
24
- /**
25
- * Check if coverage meets thresholds
26
- */
27
- checkThresholds(metrics: CoverageMetrics, threshold?: CoverageThreshold): boolean;
28
- /**
29
- * Check if a single file meets thresholds
30
- */
31
- checkFileThresholds(file: FileCoverage, threshold?: CoverageThreshold): boolean;
32
- /**
33
- * Find coverage gaps
34
- */
35
- findGaps(files: Record<string, FileCoverage>, threshold?: CoverageThreshold): CoverageGap[];
36
- /**
37
- * Calculate priority for covering a file
38
- */
39
- private calculatePriority;
40
- /**
41
- * Estimate effort to cover a file
42
- */
43
- private estimateEffort;
44
- /**
45
- * Generate test suggestions for a file
46
- */
47
- private generateSuggestions;
48
- /**
49
- * Group consecutive numbers into ranges
50
- */
51
- private groupConsecutiveNumbers;
52
- /**
53
- * Get top and bottom files by coverage
54
- */
55
- getTopFiles(files: Record<string, FileCoverage>, limit?: number): Array<{
56
- path: string;
57
- coverage: number;
58
- type: 'best' | 'worst';
59
- }>;
60
- /**
61
- * Compare two coverage results
62
- */
63
- compare(baseResult: CoverageResult, compareResult: CoverageResult): CoverageComparison;
64
- /**
65
- * Add historical coverage data
66
- */
67
- addHistory(key: string, data: HistoricalCoverage): void;
68
- /**
69
- * Get coverage trends
70
- */
71
- getTrends(key: string, type?: CoverageType, limit?: number): CoverageTrend[];
72
- /**
73
- * Calculate trend direction
74
- */
75
- getTrendDirection(trends: CoverageTrend[]): 'improving' | 'stable' | 'declining';
76
- /**
77
- * Predict future coverage based on trends
78
- */
79
- predictCoverage(key: string, type: CoverageType | undefined, targetCoverage: number): {
80
- predictedReach: number | null;
81
- projectedCoverage: number;
82
- confidence: 'high' | 'medium' | 'low';
83
- };
84
- /**
85
- * Generate coverage summary text
86
- */
87
- generateSummary(metrics: CoverageMetrics): string;
88
- /**
89
- * Format coverage percentage with color indicator
90
- */
91
- formatCoverage(percentage: number, threshold?: number): string;
92
- /**
93
- * Clear history
94
- */
95
- clearHistory(key?: string): void;
96
- }
97
- /**
98
- * Create a coverage analyzer
99
- */
100
- export declare function createCoverageAnalyzer(): CoverageAnalyzer;
101
- export {};