qa360 2.3.1 → 2.3.2

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.
@@ -184,33 +184,42 @@ export class QA360Ask {
184
184
  console.log(chalk.gray(`Analyse: "${query}"`));
185
185
  // Detection de patterns simples (Phase 1 - deterministe)
186
186
  const patterns = {
187
- api: /\b(api|rest|graphql|endpoint|swagger|openapi)\b/i,
188
- web: /\b(web|ui|interface|page|site|browser)\b/i,
189
- performance: /\b(performance|perf|load|stress|speed|latency)\b/i,
187
+ complete: /\b(complete|full|comprehensive|end-to-end|e2e|suite|tout)\b/i,
190
188
  security: /\b(security|sécurité|secu|dast|vuln|scan|pentest|zap)\b/i,
189
+ performance: /\b(performance|perf|load|stress|speed|latency|charge|charge)\b/i,
191
190
  accessibility: /\b(accessibility|a11y|accessible|wcag)\b/i,
192
- complete: /\b(complete|full|comprehensive|end-to-end|e2e|suite)\b/i
191
+ web: /\b(web|ui|interface|page|site|browser|login|auth|signin|signup|registration|connexion|inscription)\b/i,
192
+ api: /\b(api|rest|graphql|endpoint|swagger|openapi)\b/i
193
193
  };
194
194
  // URL extraction
195
195
  const urlMatch = query.match(/https?:\/\/[^\s]+/);
196
196
  const target = urlMatch ? urlMatch[0] : 'https://httpbin.org';
197
- // Type detection with priority for complete and security
198
- let type = 'api'; // default
199
- // Check for complete first (highest priority)
197
+ // Type detection with priority order
198
+ let type = 'web'; // default to web (UI) for generic queries like "login tests"
199
+ // Check patterns in priority order
200
200
  if (patterns.complete.test(query)) {
201
201
  type = 'complete';
202
202
  }
203
203
  else if (patterns.security.test(query)) {
204
204
  type = 'security';
205
205
  }
206
- else {
207
- // Check other patterns
208
- for (const [key, pattern] of Object.entries(patterns)) {
209
- if (key !== 'complete' && key !== 'security' && pattern.test(query)) {
210
- type = key;
211
- break;
212
- }
213
- }
206
+ else if (patterns.performance.test(query)) {
207
+ type = 'performance';
208
+ }
209
+ else if (patterns.accessibility.test(query)) {
210
+ type = 'accessibility';
211
+ }
212
+ else if (patterns.web.test(query)) {
213
+ type = 'web';
214
+ }
215
+ else if (patterns.api.test(query)) {
216
+ type = 'api';
217
+ }
218
+ // If no pattern matched, default to 'web' for login-like scenarios, otherwise 'api'
219
+ // Special handling for login/auth scenarios - default to web (UI) tests
220
+ const loginKeywords = /\b(login|auth|authentication|signin|signup|register|connexion|inscription|form)\b/i;
221
+ if (loginKeywords.test(query) && type === 'api') {
222
+ type = 'web';
214
223
  }
215
224
  // Pack name from query
216
225
  const name = query.toLowerCase()
@@ -316,6 +325,29 @@ export class QA360Ask {
316
325
  }
317
326
  generateWebPack(name, description, baseUrl, authConfig) {
318
327
  const auth = buildAuthConfig(authConfig);
328
+ // Check if this is a login/auth scenario based on name or description
329
+ const isLoginScenario = /\b(login|auth|signin|signup|connexion|inscription)\b/i.test(name + ' ' + description);
330
+ // Build pages based on scenario type
331
+ const pages = isLoginScenario ? [
332
+ {
333
+ url: '/login',
334
+ expectedElements: ['input[type="email"]', 'input[type="password"]', 'button[type="submit"]'],
335
+ actions: [
336
+ { type: 'fill', selector: 'input[type="email"]', value: '${TEST_EMAIL}' },
337
+ { type: 'fill', selector: 'input[type="password"]', value: '${TEST_PASSWORD}' },
338
+ { type: 'click', selector: 'button[type="submit"]' }
339
+ ]
340
+ },
341
+ {
342
+ url: '/',
343
+ expectedElements: ['header', 'nav']
344
+ }
345
+ ] : [
346
+ {
347
+ url: '/',
348
+ expectedElements: ['body', 'main']
349
+ }
350
+ ];
319
351
  return {
320
352
  version: 2,
321
353
  name,
@@ -328,12 +360,7 @@ export class QA360Ask {
328
360
  auth: auth.profileName,
329
361
  config: {
330
362
  baseUrl,
331
- pages: [
332
- {
333
- url: '/',
334
- expectedElements: ['body', 'main']
335
- }
336
- ]
363
+ pages
337
364
  }
338
365
  },
339
366
  'a11y': {
@@ -343,7 +370,7 @@ export class QA360Ask {
343
370
  baseUrl,
344
371
  pages: [
345
372
  {
346
- url: '/',
373
+ url: isLoginScenario ? '/login' : '/',
347
374
  a11yRules: ['wcag2a', 'wcag2aa']
348
375
  }
349
376
  ]
@@ -18,7 +18,14 @@ coverageCommand
18
18
  .option('--verbose', 'Show detailed file-by-file coverage')
19
19
  .action(async (options) => {
20
20
  if (!options.file && !options.run) {
21
- console.error('Error: Please specify --file or --run');
21
+ console.error('Error: Please specify --file or --run to show coverage\n');
22
+ console.error('Usage: qa360 coverage show --file <path>');
23
+ console.error(' or: qa360 coverage show --run <id>\n');
24
+ console.error('Examples:');
25
+ console.error(' qa360 coverage show --file coverage/coverage-final.json');
26
+ console.error(' qa360 coverage show --file coverage/lcov.info');
27
+ console.error(' qa360 coverage show --run abc123\n');
28
+ console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
22
29
  process.exit(1);
23
30
  }
24
31
  const collector = new CoverageCollector();
@@ -82,7 +89,10 @@ coverageCommand
82
89
  .option('--json', 'Output as JSON')
83
90
  .action(async (options) => {
84
91
  if (!options.file) {
85
- console.error('Error: Please specify --file');
92
+ console.error('Error: Please specify --file to analyze coverage\n');
93
+ console.error('Usage: qa360 coverage analyze --file <path>\n');
94
+ console.error('Example: qa360 coverage analyze --file coverage/coverage-final.json\n');
95
+ console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
86
96
  process.exit(1);
87
97
  }
88
98
  if (!existsSync(options.file)) {
@@ -218,7 +228,10 @@ coverageCommand
218
228
  .option('--fail', 'Exit with error code if thresholds not met')
219
229
  .action(async (options) => {
220
230
  if (!options.file) {
221
- console.error('Error: Please specify --file');
231
+ console.error('Error: Please specify --file to check coverage\n');
232
+ console.error('Usage: qa360 coverage check --file <path>\n');
233
+ console.error('Example: qa360 coverage check --file coverage/coverage-final.json\n');
234
+ console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
222
235
  process.exit(1);
223
236
  }
224
237
  if (!existsSync(options.file)) {
@@ -91,10 +91,17 @@ export async function generateCommand(source, options = {}) {
91
91
  const { available, models, recommended } = await checkGenerationAvailability();
92
92
  if (!available) {
93
93
  spinner.fail('Ollama not available');
94
- console.log(chalk.red('\nOllama is not running'));
95
- console.log(chalk.yellow('\nStart Ollama:'));
96
- console.log(chalk.gray(' ollama serve'));
97
- console.log(chalk.yellow('\nInstall Ollama:'));
94
+ console.log(chalk.red('\n❌ Ollama is not running or not reachable'));
95
+ console.log(chalk.yellow('\n🔧 Troubleshooting:'));
96
+ console.log(chalk.gray(' 1. Check if Ollama is running:'));
97
+ console.log(chalk.cyan(' ollama list'));
98
+ console.log(chalk.gray(' 2. Start Ollama if needed:'));
99
+ console.log(chalk.cyan(' ollama serve'));
100
+ console.log(chalk.gray(' 3. Test QA360 connection to Ollama:'));
101
+ console.log(chalk.cyan(' qa360 ollama test'));
102
+ console.log(chalk.gray(' 4. Set custom Ollama URL if needed:'));
103
+ console.log(chalk.cyan(' qa360 ollama config --set-url http://localhost:11434'));
104
+ console.log(chalk.yellow('\n📦 Install Ollama:'));
98
105
  console.log(chalk.gray(' brew install ollama # macOS'));
99
106
  console.log(chalk.gray(' # Or visit: https://ollama.com\n'));
100
107
  return;
@@ -19,12 +19,20 @@ export async function ollamaTestCommand(options = {}) {
19
19
  const check = await checkOllamaSetup(model);
20
20
  if (!check.available) {
21
21
  spinner.fail('Ollama is not available');
22
- console.log(chalk.red('\n❌ Ollama is not running or not installed'));
22
+ console.log(chalk.red('\n❌ Ollama is not running or not reachable'));
23
+ console.log(chalk.yellow('\n🔧 Troubleshooting steps:'));
24
+ console.log(chalk.gray('\n 1. Verify Ollama is running:'));
25
+ console.log(chalk.cyan(' ollama list'));
26
+ console.log(chalk.gray('\n 2. Start Ollama if not running:'));
27
+ console.log(chalk.cyan(' ollama serve'));
28
+ console.log(chalk.gray('\n 3. Check Ollama is listening on default port:'));
29
+ console.log(chalk.cyan(' curl http://localhost:11434/api/tags'));
30
+ console.log(chalk.gray('\n 4. Set custom URL if Ollama is on different port/host:'));
31
+ console.log(chalk.cyan(' qa360 ollama config --set-url http://localhost:11434'));
32
+ console.log(chalk.gray('\n 5. Check for firewall/proxy issues blocking localhost:11434'));
23
33
  console.log(chalk.yellow('\n📦 To install Ollama:'));
24
34
  console.log(chalk.gray(' brew install ollama # macOS'));
25
- console.log(chalk.gray(' # Or visit: https://ollama.com'));
26
- console.log(chalk.yellow('\n🚀 To start Ollama:'));
27
- console.log(chalk.gray(' ollama serve'));
35
+ console.log(chalk.gray(' # Or visit: https://ollama.com\n'));
28
36
  return;
29
37
  }
30
38
  spinner.succeed('Ollama is running!');
@@ -175,7 +183,7 @@ export async function ollamaGenerateCommand(prompt, options = {}) {
175
183
  export async function ollamaConfigCommand(options = {}) {
176
184
  const config = loadQA360Config();
177
185
  const ollamaConfig = config.ollama || {};
178
- const defaultBaseUrl = 'http://localhost:11434';
186
+ const defaultBaseUrl = 'http://127.0.0.1:11434';
179
187
  const defaultModel = 'deepseek-coder';
180
188
  // Update config if options provided
181
189
  if (options.setUrl || options.setModel) {
@@ -39,3 +39,7 @@ export declare function displayResults(result: Phase3RunResult): void;
39
39
  */
40
40
  export declare function runCommand(packArg?: string, options?: RunOptions): Promise<void>;
41
41
  export default runCommand;
42
+ /**
43
+ * Preflight command - Validate pack without executing tests
44
+ */
45
+ export declare function preflightCommand(packArg?: string): Promise<void>;
@@ -170,3 +170,89 @@ export async function runCommand(packArg, options = {}) {
170
170
  }
171
171
  // Export for CLI integration
172
172
  export default runCommand;
173
+ /**
174
+ * Preflight command - Validate pack without executing tests
175
+ */
176
+ export async function preflightCommand(packArg) {
177
+ try {
178
+ console.log(chalk.blue('\n🔍 QA360 Preflight - Dry-run validation\n'));
179
+ // Step 1: Find pack file
180
+ const packPath = findPackFile(packArg);
181
+ console.log(chalk.gray(`Pack file: ${packPath}`));
182
+ // Step 2: Load and validate pack
183
+ const pack = await loadPack(packPath);
184
+ console.log(chalk.green(`✅ Pack: ${pack.name} v${pack.version}`));
185
+ // Step 3: Validate structure
186
+ const issues = [];
187
+ // Check required fields
188
+ if (!pack.name) {
189
+ issues.push('Missing required field: name');
190
+ }
191
+ if (!pack.version) {
192
+ issues.push('Missing required field: version');
193
+ }
194
+ if (!pack.gates || Object.keys(pack.gates).length === 0) {
195
+ issues.push('No gates defined - nothing to test');
196
+ }
197
+ // Validate gates
198
+ const gateNames = Object.keys(pack.gates || {});
199
+ if (gateNames.length > 0) {
200
+ console.log(chalk.gray(`\nGates (${gateNames.length}):`));
201
+ for (const [gateName, gateConfig] of Object.entries(pack.gates || {})) {
202
+ const gate = gateConfig;
203
+ const status = gate.enabled === false ? chalk.yellow('(disabled)') : chalk.green('(enabled)');
204
+ console.log(` ${status} ${chalk.cyan(gateName)}`);
205
+ if (!gate.adapter) {
206
+ issues.push(` ⚠️ Gate "${gateName}" missing adapter`);
207
+ }
208
+ else {
209
+ console.log(` Adapter: ${gate.adapter}`);
210
+ }
211
+ // Show pages/config summary
212
+ if (gate.config?.pages) {
213
+ const pages = gate.config.pages;
214
+ const pageCount = Array.isArray(pages) ? pages.length : Object.keys(pages).length;
215
+ console.log(` Pages: ${pageCount}`);
216
+ }
217
+ if (gate.config?.smoke) {
218
+ const smoke = gate.config.smoke;
219
+ const testCount = Array.isArray(smoke) ? smoke.length : 1;
220
+ console.log(` Smoke tests: ${testCount}`);
221
+ }
222
+ }
223
+ }
224
+ // Validate auth config (only for v2 packs)
225
+ if (pack.version === 2 && pack.auth) {
226
+ const auth = pack.auth;
227
+ console.log(chalk.gray('\nAuthentication:'));
228
+ if (auth.api) {
229
+ console.log(` API: ${auth.api}`);
230
+ }
231
+ if (auth.ui) {
232
+ console.log(` UI: ${auth.ui}`);
233
+ }
234
+ if (auth.profiles) {
235
+ const profileNames = Object.keys(auth.profiles);
236
+ console.log(` Profiles: ${profileNames.join(', ') || 'none'}`);
237
+ }
238
+ }
239
+ // Summary
240
+ console.log(chalk.bold('\n' + '─'.repeat(50)));
241
+ if (issues.length === 0) {
242
+ console.log(chalk.green('✅ Preflight check passed'));
243
+ console.log(chalk.gray(' Run "qa360 run" to execute tests'));
244
+ }
245
+ else {
246
+ console.log(chalk.yellow('⚠️ Issues found:'));
247
+ for (const issue of issues) {
248
+ console.log(chalk.yellow(` ${issue}`));
249
+ }
250
+ }
251
+ console.log('');
252
+ }
253
+ catch (error) {
254
+ console.error(chalk.red('\n❌ Preflight validation failed:'));
255
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
256
+ process.exit(1);
257
+ }
258
+ }
@@ -26,6 +26,10 @@ export declare class OllamaProvider implements LLMProvider {
26
26
  private readonly timeout;
27
27
  constructor(config?: OllamaConfig);
28
28
  isAvailable(): Promise<boolean>;
29
+ /**
30
+ * Ensure URL has valid format (http:// prefix)
31
+ */
32
+ private ensureValidUrl;
29
33
  generate(request: GenerationRequest): Promise<GenerationResponse>;
30
34
  stream(request: GenerationRequest): AsyncIterable<string>;
31
35
  countTokens(text: string): number;
@@ -26,17 +26,20 @@ export class OllamaProvider {
26
26
  defaultModel;
27
27
  timeout;
28
28
  constructor(config = {}) {
29
- this.baseUrl = config.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
29
+ // Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues
30
+ this.baseUrl = config.baseUrl || process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434';
30
31
  this.defaultModel = config.model || process.env.OLLAMA_MODEL || 'deepseek-coder';
31
32
  this.timeout = config.timeout || 120000; // 2 minutes default
32
33
  }
33
34
  async isAvailable() {
34
35
  try {
36
+ // Ensure URL has proper format
37
+ const url = this.ensureValidUrl(this.baseUrl);
35
38
  // Use a longer timeout (15 seconds) to accommodate slower systems
36
39
  // Ollama can take time to respond, especially on first run or with many models
37
40
  const controller = new AbortController();
38
41
  const timeoutId = setTimeout(() => controller.abort(), 15000);
39
- const response = await fetch(`${this.baseUrl}/api/tags`, {
42
+ const response = await fetch(`${url}/api/tags`, {
40
43
  signal: controller.signal,
41
44
  });
42
45
  clearTimeout(timeoutId);
@@ -49,21 +52,37 @@ export class OllamaProvider {
49
52
  // Timeout - Ollama is slow to respond
50
53
  console.debug(`[Ollama] Connection to ${this.baseUrl} timed out after 15s`);
51
54
  }
55
+ else if (error.message.includes('ECONNREFUSED')) {
56
+ console.debug(`[Ollama] Connection refused - Ollama may not be running at ${this.baseUrl}`);
57
+ }
58
+ else if (error.message.includes('ENOTFOUND')) {
59
+ console.debug(`[Ollama] Host not found - check Ollama URL: ${this.baseUrl}`);
60
+ }
52
61
  else {
53
- // Other error (ECONNREFUSED, ENOTFOUND, etc.)
62
+ // Other error
54
63
  console.debug(`[Ollama] Connection error: ${error.message}`);
55
64
  }
56
65
  }
57
66
  return false;
58
67
  }
59
68
  }
69
+ /**
70
+ * Ensure URL has valid format (http:// prefix)
71
+ */
72
+ ensureValidUrl(url) {
73
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
74
+ return `http://${url}`;
75
+ }
76
+ return url;
77
+ }
60
78
  async generate(request) {
61
79
  const fullPrompt = this.buildFullPrompt(request);
80
+ const url = this.ensureValidUrl(this.baseUrl);
62
81
  // Use timeout with AbortController for compatibility
63
82
  const controller = new AbortController();
64
83
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
65
84
  try {
66
- const response = await fetch(`${this.baseUrl}/api/generate`, {
85
+ const response = await fetch(`${url}/api/generate`, {
67
86
  method: 'POST',
68
87
  headers: { 'Content-Type': 'application/json' },
69
88
  signal: controller.signal,
@@ -101,7 +120,8 @@ export class OllamaProvider {
101
120
  }
102
121
  async *stream(request) {
103
122
  const fullPrompt = this.buildFullPrompt(request);
104
- const response = await fetch(`${this.baseUrl}/api/generate`, {
123
+ const url = this.ensureValidUrl(this.baseUrl);
124
+ const response = await fetch(`${url}/api/generate`, {
105
125
  method: 'POST',
106
126
  headers: { 'Content-Type': 'application/json' },
107
127
  body: JSON.stringify({
@@ -153,7 +173,8 @@ export class OllamaProvider {
153
173
  * List available models from Ollama
154
174
  */
155
175
  async listModels() {
156
- const response = await fetch(`${this.baseUrl}/api/tags`);
176
+ const url = this.ensureValidUrl(this.baseUrl);
177
+ const response = await fetch(`${url}/api/tags`);
157
178
  if (!response.ok) {
158
179
  throw new OllamaError('Failed to list models');
159
180
  }
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import { doctorCommand } from './commands/doctor.js';
35
35
  import { askCommand } from './commands/ask.js';
36
36
  import { initCommand } from './commands/init.js';
37
37
  import { examplesListCommand, examplesCopyCommand, examplesShowCommand } from './commands/examples.js';
38
- import { runCommand } from './commands/run.js';
38
+ import { runCommand, preflightCommand } from './commands/run.js';
39
39
  import { reportCommand } from './commands/report.js';
40
40
  import { verifyCommand } from './commands/verify.js';
41
41
  import { explainCommand } from './commands/explain.js';
@@ -125,8 +125,7 @@ program
125
125
  .command('preflight [pack]')
126
126
  .description('Dry-run validation without execution')
127
127
  .action(async (pack) => {
128
- console.log(chalk.blue('🔍 QA360 Preflight - Validation'));
129
- console.log(chalk.yellow('⚠️ Phase 2 implementation coming soon...'));
128
+ await preflightCommand(pack);
130
129
  });
131
130
  program
132
131
  .command('report')
@@ -109,7 +109,31 @@ function parseYaml(content) {
109
109
  if (listMatch) {
110
110
  const value = listMatch[1].trim();
111
111
  if (Array.isArray(parent)) {
112
- parent.push(parseValue(value));
112
+ // Check if this is an object item (key: value) or a simple value
113
+ const colonIdx = value.indexOf(':');
114
+ if (colonIdx > 0 && !value.startsWith("'") && !value.startsWith('"')) {
115
+ // Object item like "- url: /" or "- key: value"
116
+ const objKey = value.slice(0, colonIdx).trim();
117
+ let objValue = value.slice(colonIdx + 1).trim();
118
+ // Check if next line is more indented (nested object properties)
119
+ if (i < lines.length - 1) {
120
+ const nextIndent = lines[i + 1].search(/\S|$/);
121
+ if (nextIndent > indent) {
122
+ // This is the start of a nested object
123
+ const obj = {};
124
+ obj[objKey] = parseValue(objValue);
125
+ parent.push(obj);
126
+ stack.push({ obj, indent });
127
+ continue;
128
+ }
129
+ }
130
+ // Single-line object item
131
+ parent.push({ [objKey]: parseValue(objValue) });
132
+ }
133
+ else {
134
+ // Simple value item
135
+ parent.push(parseValue(value));
136
+ }
113
137
  }
114
138
  continue;
115
139
  }
@@ -144,6 +168,9 @@ function parseYaml(content) {
144
168
  if (i < lines.length - 1) {
145
169
  const nextIndent = lines[i + 1].search(/\S|$/);
146
170
  if (nextIndent > indent) {
171
+ const nextLine = lines[i + 1].trim();
172
+ // Check if next line starts a list (array) or a key-value (object)
173
+ const isList = nextLine.startsWith('-');
147
174
  if (Array.isArray(parent[key])) {
148
175
  // Already an array
149
176
  }
@@ -151,12 +178,18 @@ function parseYaml(content) {
151
178
  // Nested object
152
179
  stack.push({ obj: parent[key], indent });
153
180
  }
154
- else {
155
- // Start of array
181
+ else if (isList) {
182
+ // Start of array (next line is a list item)
156
183
  const arr = [];
157
184
  parent[key] = arr;
158
185
  stack.push({ obj: arr, indent });
159
186
  }
187
+ else {
188
+ // Start of object (next line is a key-value pair)
189
+ const obj = {};
190
+ parent[key] = obj;
191
+ stack.push({ obj, indent });
192
+ }
160
193
  }
161
194
  }
162
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",