mcp-lab-agent 2.0.0 → 2.1.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/dist/index.js CHANGED
@@ -5,21 +5,27 @@ import { config } from "dotenv";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { z } from "zod";
8
- import { spawn } from "child_process";
9
- import path from "path";
10
- import fs from "fs";
11
- var PROJECT_ROOT = process.cwd();
12
- config({ path: path.join(PROJECT_ROOT, ".env") });
13
- var server = new McpServer({
14
- name: "mcp-lab-agent",
15
- version: "1.0.0"
16
- });
8
+ import { spawn as spawn2 } from "child_process";
9
+ import path5 from "path";
10
+ import fs5 from "fs";
11
+
12
+ // src/core/llm-router.js
17
13
  function resolveLLMProvider(taskType = "simple") {
18
14
  const GROQ_KEY = process.env.GROQ_API_KEY;
19
15
  const GEMINI_KEY = process.env.GEMINI_API_KEY;
20
16
  const OPENAI_KEY = process.env.OPENAI_API_KEY || process.env.QA_LAB_LLM_API_KEY;
17
+ const OLLAMA_URL = process.env.OLLAMA_BASE_URL || "http://localhost:11434";
18
+ const CUSTOM_URL = process.env.QA_LAB_LLM_BASE_URL;
21
19
  const simpleModel = process.env.QA_LAB_LLM_SIMPLE;
22
20
  const complexModel = process.env.QA_LAB_LLM_COMPLEX;
21
+ if (CUSTOM_URL) {
22
+ const model2 = taskType === "complex" ? complexModel || "llama3.1:70b" : simpleModel || "llama3.1:8b";
23
+ return { provider: "custom", apiKey: process.env.QA_LAB_LLM_API_KEY || "not-needed", baseUrl: CUSTOM_URL, model: model2 };
24
+ }
25
+ if (!GROQ_KEY && !GEMINI_KEY && !OPENAI_KEY) {
26
+ const model2 = taskType === "complex" ? complexModel || "llama3.1:70b" : simpleModel || "llama3.1:8b";
27
+ return { provider: "ollama", apiKey: "not-needed", baseUrl: `${OLLAMA_URL}/v1`, model: model2 };
28
+ }
23
29
  let provider = GROQ_KEY ? "groq" : GEMINI_KEY ? "gemini" : "openai";
24
30
  const apiKey = GROQ_KEY || GEMINI_KEY || OPENAI_KEY;
25
31
  const baseUrl = provider === "groq" ? "https://api.groq.com/openai/v1" : provider === "gemini" ? "https://generativelanguage.googleapis.com/v1beta" : "https://api.openai.com/v1";
@@ -31,8 +37,13 @@ function resolveLLMProvider(taskType = "simple") {
31
37
  }
32
38
  return { provider, apiKey, baseUrl, model };
33
39
  }
40
+
41
+ // src/core/memory.js
42
+ import path from "path";
43
+ import fs from "fs";
44
+ var PROJECT_ROOT = process.cwd();
34
45
  var MEMORY_FILE = path.join(PROJECT_ROOT, ".qa-lab-memory.json");
35
- var FLOWS_CONFIG_FILE = path.join(PROJECT_ROOT, "qa-lab-flows.json");
46
+ var FLOWS_CONFIG_FILE2 = path.join(PROJECT_ROOT, "qa-lab-flows.json");
36
47
  function loadProjectMemory() {
37
48
  const memory = { patterns: {}, conventions: {}, lastRun: null, selectors: [] };
38
49
  if (fs.existsSync(MEMORY_FILE)) {
@@ -42,9 +53,9 @@ function loadProjectMemory() {
42
53
  } catch {
43
54
  }
44
55
  }
45
- if (fs.existsSync(FLOWS_CONFIG_FILE)) {
56
+ if (fs.existsSync(FLOWS_CONFIG_FILE2)) {
46
57
  try {
47
- const flows = JSON.parse(fs.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
58
+ const flows = JSON.parse(fs.readFileSync(FLOWS_CONFIG_FILE2, "utf8"));
48
59
  memory.flows = flows.flows || [];
49
60
  } catch {
50
61
  }
@@ -63,6 +74,11 @@ function saveProjectMemory(updates) {
63
74
  data.learnings.push(...updates.learnings);
64
75
  if (data.learnings.length > 200) data.learnings = data.learnings.slice(-150);
65
76
  }
77
+ if (updates.execution) {
78
+ data.executions = data.executions || [];
79
+ data.executions.push(updates.execution);
80
+ if (data.executions.length > 500) data.executions = data.executions.slice(-300);
81
+ }
66
82
  data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
67
83
  fs.writeFileSync(MEMORY_FILE, JSON.stringify(data, null, 2), "utf8");
68
84
  } catch {
@@ -85,6 +101,38 @@ function getMemoryStats() {
85
101
  firstAttemptSuccessRate: totalTests > 0 ? Math.round(firstAttemptSuccess / totalTests * 100) : 0
86
102
  };
87
103
  }
104
+ function analyzeTestStability() {
105
+ const memory = loadProjectMemory();
106
+ const executions = memory.executions || [];
107
+ if (executions.length === 0) return { tests: [], message: "Nenhuma execu\xE7\xE3o registrada ainda." };
108
+ const byTest = {};
109
+ executions.forEach((ex) => {
110
+ if (!byTest[ex.testFile]) {
111
+ byTest[ex.testFile] = { total: 0, passed: 0, failed: 0, durations: [] };
112
+ }
113
+ byTest[ex.testFile].total++;
114
+ if (ex.passed) byTest[ex.testFile].passed++;
115
+ else byTest[ex.testFile].failed++;
116
+ if (ex.duration) byTest[ex.testFile].durations.push(ex.duration);
117
+ });
118
+ const tests = Object.entries(byTest).map(([file, data]) => {
119
+ const failureRate = Math.round(data.failed / data.total * 100);
120
+ const avgDuration = data.durations.length > 0 ? (data.durations.reduce((a, b) => a + b, 0) / data.durations.length).toFixed(1) : 0;
121
+ const stability = failureRate === 0 ? "stable" : failureRate < 20 ? "mostly_stable" : failureRate < 50 ? "flaky" : "unstable";
122
+ return {
123
+ file,
124
+ total: data.total,
125
+ passed: data.passed,
126
+ failed: data.failed,
127
+ failureRate,
128
+ avgDuration: parseFloat(avgDuration),
129
+ stability
130
+ };
131
+ }).sort((a, b) => b.failureRate - a.failureRate);
132
+ return { tests, message: `Analisadas ${executions.length} execu\xE7\xF5es de ${tests.length} teste(s).` };
133
+ }
134
+
135
+ // src/core/flaky-detection.js
88
136
  var FLAKY_PATTERNS = [
89
137
  { name: "timing", regex: /timeout|timed out|exceeded|wait|delay|slow|race condition/i, suggestion: "Adicione wait expl\xEDcito (ex: page.waitForSelector) ou aumente o timeout." },
90
138
  { name: "ordering", regex: /order|sequenc|flaky|intermittent|sometimes|random/i, suggestion: "Issole o teste ou use beforeAll/afterAll para estado limpo. Evite depend\xEAncia de ordem entre testes." },
@@ -102,6 +150,11 @@ function detectFlakyPatterns(runOutput) {
102
150
  const confidence = detected.length > 0 ? Math.min(0.5 + detected.length * 0.2, 0.95) : 0;
103
151
  return { isLikelyFlaky: confidence > 0.5, confidence, patterns: detected };
104
152
  }
153
+
154
+ // src/core/project-structure.js
155
+ import path2 from "path";
156
+ import fs2 from "fs";
157
+ var PROJECT_ROOT2 = process.cwd();
105
158
  function detectProjectStructure() {
106
159
  const structure = {
107
160
  hasTests: false,
@@ -115,9 +168,9 @@ function detectProjectStructure() {
115
168
  packageJson: null,
116
169
  pythonRequirements: null
117
170
  };
118
- const pkgPath = path.join(PROJECT_ROOT, "package.json");
119
- if (fs.existsSync(pkgPath)) {
120
- structure.packageJson = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
171
+ const pkgPath = path2.join(PROJECT_ROOT2, "package.json");
172
+ if (fs2.existsSync(pkgPath)) {
173
+ structure.packageJson = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
121
174
  const deps = {
122
175
  ...structure.packageJson.dependencies,
123
176
  ...structure.packageJson.devDependencies
@@ -191,9 +244,9 @@ function detectProjectStructure() {
191
244
  structure.hasFrontend = true;
192
245
  }
193
246
  }
194
- const requirementsPath = path.join(PROJECT_ROOT, "requirements.txt");
195
- if (fs.existsSync(requirementsPath)) {
196
- const requirements = fs.readFileSync(requirementsPath, "utf8");
247
+ const requirementsPath = path2.join(PROJECT_ROOT2, "requirements.txt");
248
+ if (fs2.existsSync(requirementsPath)) {
249
+ const requirements = fs2.readFileSync(requirementsPath, "utf8");
197
250
  structure.pythonRequirements = requirements;
198
251
  if (/robotframework/i.test(requirements)) {
199
252
  structure.testFrameworks.push("robot");
@@ -228,7 +281,6 @@ function detectProjectStructure() {
228
281
  "scenarios",
229
282
  "mobile",
230
283
  "api",
231
- // Monorepo: subprojetos por framework
232
284
  "playwright-js",
233
285
  "puppeteer-js",
234
286
  "testcafe-js",
@@ -239,20 +291,20 @@ function detectProjectStructure() {
239
291
  "selenium-python"
240
292
  ];
241
293
  for (const dir of commonTestDirs) {
242
- const fullPath = path.join(PROJECT_ROOT, dir);
243
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
294
+ const fullPath = path2.join(PROJECT_ROOT2, dir);
295
+ if (fs2.existsSync(fullPath) && fs2.statSync(fullPath).isDirectory()) {
244
296
  structure.testDirs.push(dir);
245
297
  }
246
298
  }
247
299
  const skipDirs = ["node_modules", ".git", "dist", "build", ".next", ".venv"];
248
300
  try {
249
- const rootEntries = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
301
+ const rootEntries = fs2.readdirSync(PROJECT_ROOT2, { withFileTypes: true });
250
302
  for (const e of rootEntries) {
251
303
  if (!e.isDirectory() || skipDirs.includes(e.name)) continue;
252
- const subPath = path.join(PROJECT_ROOT, e.name);
304
+ const subPath = path2.join(PROJECT_ROOT2, e.name);
253
305
  if (structure.testDirs.includes(e.name)) continue;
254
- const hasPkg = fs.existsSync(path.join(subPath, "package.json"));
255
- const hasTests = fs.existsSync(path.join(subPath, "tests")) || fs.existsSync(path.join(subPath, "test")) || fs.existsSync(path.join(subPath, "e2e")) || fs.existsSync(path.join(subPath, "__tests__")) || fs.existsSync(path.join(subPath, "specs"));
306
+ const hasPkg = fs2.existsSync(path2.join(subPath, "package.json"));
307
+ const hasTests = fs2.existsSync(path2.join(subPath, "tests")) || fs2.existsSync(path2.join(subPath, "test")) || fs2.existsSync(path2.join(subPath, "e2e")) || fs2.existsSync(path2.join(subPath, "__tests__")) || fs2.existsSync(path2.join(subPath, "specs"));
256
308
  if (hasPkg || hasTests) {
257
309
  structure.testDirs.push(e.name);
258
310
  }
@@ -260,10 +312,10 @@ function detectProjectStructure() {
260
312
  } catch {
261
313
  }
262
314
  for (const dir of structure.testDirs) {
263
- const subPkg = path.join(PROJECT_ROOT, dir, "package.json");
264
- if (!fs.existsSync(subPkg)) continue;
315
+ const subPkg = path2.join(PROJECT_ROOT2, dir, "package.json");
316
+ if (!fs2.existsSync(subPkg)) continue;
265
317
  try {
266
- const sub = JSON.parse(fs.readFileSync(subPkg, "utf8"));
318
+ const sub = JSON.parse(fs2.readFileSync(subPkg, "utf8"));
267
319
  const subDeps = { ...sub.dependencies || {}, ...sub.devDependencies || {} };
268
320
  const toAdd = [];
269
321
  if (subDeps.cypress && !structure.testFrameworks.includes("cypress")) toAdd.push("cypress");
@@ -282,10 +334,10 @@ function detectProjectStructure() {
282
334
  }
283
335
  }
284
336
  for (const dir of structure.testDirs) {
285
- const reqPath = path.join(PROJECT_ROOT, dir, "requirements.txt");
286
- if (!fs.existsSync(reqPath)) continue;
337
+ const reqPath = path2.join(PROJECT_ROOT2, dir, "requirements.txt");
338
+ if (!fs2.existsSync(reqPath)) continue;
287
339
  try {
288
- const req = fs.readFileSync(reqPath, "utf8");
340
+ const req = fs2.readFileSync(reqPath, "utf8");
289
341
  if (/robotframework/i.test(req) && !structure.testFrameworks.includes("robot")) {
290
342
  structure.testFrameworks.push("robot");
291
343
  structure.hasTests = true;
@@ -299,9 +351,9 @@ function detectProjectStructure() {
299
351
  }
300
352
  const commonBackendDirs = ["backend", "server", "api", "src"];
301
353
  for (const dir of commonBackendDirs) {
302
- const fullPath = path.join(PROJECT_ROOT, dir);
303
- if (fs.existsSync(fullPath) && !structure.backendDir) {
304
- const hasServerFile = fs.existsSync(path.join(fullPath, "server.js")) || fs.existsSync(path.join(fullPath, "index.js")) || fs.existsSync(path.join(fullPath, "app.js"));
354
+ const fullPath = path2.join(PROJECT_ROOT2, dir);
355
+ if (fs2.existsSync(fullPath) && !structure.backendDir) {
356
+ const hasServerFile = fs2.existsSync(path2.join(fullPath, "server.js")) || fs2.existsSync(path2.join(fullPath, "index.js")) || fs2.existsSync(path2.join(fullPath, "app.js"));
305
357
  if (hasServerFile) {
306
358
  structure.backendDir = dir;
307
359
  }
@@ -309,9 +361,9 @@ function detectProjectStructure() {
309
361
  }
310
362
  const commonFrontendDirs = ["frontend", "client", "web", "app", "src"];
311
363
  for (const dir of commonFrontendDirs) {
312
- const fullPath = path.join(PROJECT_ROOT, dir);
313
- if (fs.existsSync(fullPath) && !structure.frontendDir) {
314
- const hasAppFile = fs.existsSync(path.join(fullPath, "App.js")) || fs.existsSync(path.join(fullPath, "App.tsx")) || fs.existsSync(path.join(fullPath, "index.html"));
364
+ const fullPath = path2.join(PROJECT_ROOT2, dir);
365
+ if (fs2.existsSync(fullPath) && !structure.frontendDir) {
366
+ const hasAppFile = fs2.existsSync(path2.join(fullPath, "App.js")) || fs2.existsSync(path2.join(fullPath, "App.tsx")) || fs2.existsSync(path2.join(fullPath, "index.html"));
315
367
  if (hasAppFile) {
316
368
  structure.frontendDir = dir;
317
369
  }
@@ -322,7 +374,6 @@ function detectProjectStructure() {
322
374
  var UNIVERSAL_TEST_PATTERNS = [
323
375
  /\.(cy|spec|test)\.(js|ts|jsx|tsx)$/i,
324
376
  /_test\.(js|ts)$/i,
325
- // CodeceptJS
326
377
  /\.robot$/i,
327
378
  /\.feature$/i,
328
379
  /^(test_.*|.*_test)\.py$/i,
@@ -337,15 +388,15 @@ function collectTestFiles(structure, options = {}) {
337
388
  const { pattern, framework, maxContentFiles = 0 } = options;
338
389
  const results = [];
339
390
  for (const dir of structure.testDirs) {
340
- const fullPath = path.join(PROJECT_ROOT, dir);
391
+ const fullPath = path2.join(PROJECT_ROOT2, dir);
341
392
  const walk = (p, base = "") => {
342
- if (!fs.existsSync(p)) return;
343
- const entries = fs.readdirSync(p, { withFileTypes: true });
393
+ if (!fs2.existsSync(p)) return;
394
+ const entries = fs2.readdirSync(p, { withFileTypes: true });
344
395
  for (const e of entries) {
345
396
  const rel = base ? `${base}/${e.name}` : e.name;
346
397
  if (e.isDirectory()) {
347
398
  if (e.name === "node_modules" || e.name === ".git" || e.name === ".venv") continue;
348
- walk(path.join(p, e.name), rel);
399
+ walk(path2.join(p, e.name), rel);
349
400
  } else if (e.isFile() && isTestFile(e.name)) {
350
401
  const filePath = `${dir}/${rel}`;
351
402
  if (pattern && !filePath.toLowerCase().includes(pattern.toLowerCase())) continue;
@@ -354,7 +405,7 @@ function collectTestFiles(structure, options = {}) {
354
405
  const entry = { path: filePath, inferredFramework: inferredFw };
355
406
  if (maxContentFiles > 0 && results.length < maxContentFiles) {
356
407
  try {
357
- entry.content = fs.readFileSync(path.join(PROJECT_ROOT, filePath), "utf8");
408
+ entry.content = fs2.readFileSync(path2.join(PROJECT_ROOT2, filePath), "utf8");
358
409
  } catch {
359
410
  }
360
411
  }
@@ -401,13 +452,46 @@ function matchesFramework(inferred, requested) {
401
452
  function getFrameworkCwd(structure, preferredDirs) {
402
453
  for (const dir of preferredDirs) {
403
454
  if (structure.testDirs.includes(dir)) {
404
- return path.join(PROJECT_ROOT, dir);
455
+ return path2.join(PROJECT_ROOT2, dir);
405
456
  }
406
457
  }
407
458
  const fallback = structure.testDirs[0];
408
- return fallback ? path.join(PROJECT_ROOT, fallback) : PROJECT_ROOT;
459
+ return fallback ? path2.join(PROJECT_ROOT2, fallback) : PROJECT_ROOT2;
460
+ }
461
+ function analyzeCodeRisks() {
462
+ const structure = detectProjectStructure();
463
+ const risks = [];
464
+ const srcDirs = ["src", "app", "lib", "components", "pages", "api", "services", "controllers"];
465
+ const foundDirs = srcDirs.filter((dir) => fs2.existsSync(path2.join(PROJECT_ROOT2, dir)));
466
+ foundDirs.forEach((dir) => {
467
+ const fullPath = path2.join(PROJECT_ROOT2, dir);
468
+ const files = fs2.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(js|ts|jsx|tsx|py)$/.test(f));
469
+ const hasTests = structure.testDirs.some((testDir) => {
470
+ const testPath = path2.join(PROJECT_ROOT2, testDir);
471
+ if (!fs2.existsSync(testPath)) return false;
472
+ const testFiles = fs2.readdirSync(testPath, { recursive: true });
473
+ return testFiles.some((tf) => tf.includes(dir) || tf.toLowerCase().includes(dir.toLowerCase()));
474
+ });
475
+ if (!hasTests && files.length > 0) {
476
+ risks.push({
477
+ area: dir,
478
+ files: files.length,
479
+ risk: files.length > 20 ? "high" : files.length > 10 ? "medium" : "low",
480
+ reason: "Sem testes detectados para esta \xE1rea"
481
+ });
482
+ }
483
+ });
484
+ return risks.sort((a, b) => {
485
+ const riskOrder = { high: 3, medium: 2, low: 1 };
486
+ return riskOrder[b.risk] - riskOrder[a.risk];
487
+ });
409
488
  }
410
- var METRICS_FILE = path.join(PROJECT_ROOT, ".qa-lab-metrics.json");
489
+
490
+ // src/core/tool-helpers.js
491
+ import path3 from "path";
492
+ import fs3 from "fs";
493
+ var PROJECT_ROOT3 = process.cwd();
494
+ var METRICS_FILE = path3.join(PROJECT_ROOT3, ".qa-lab-metrics.json");
411
495
  function parseTestRunResult(runOutput, exitCode) {
412
496
  let passed = 0;
413
497
  let failed = 0;
@@ -416,25 +500,13 @@ function parseTestRunResult(runOutput, exitCode) {
416
500
  passed = parseInt(jestMatch[1], 10);
417
501
  failed = jestMatch[2] ? parseInt(jestMatch[2], 10) : 0;
418
502
  }
419
- const cypressPass = runOutput.match(/(\d+)\s+passing/);
420
- const cypressFail = runOutput.match(/(\d+)\s+failing/);
421
- if (cypressPass) passed = parseInt(cypressPass[1], 10);
422
- if (cypressFail) failed = parseInt(cypressFail[1], 10);
423
- const pwPass = runOutput.match(/(\d+)\s+passed/);
424
- const pwFail = runOutput.match(/(\d+)\s+failed/);
425
- if (pwPass) passed = parseInt(pwPass[1], 10);
426
- if (pwFail) failed = parseInt(pwFail[1], 10);
427
- if (passed === 0 && failed === 0) {
428
- if (exitCode === 0) passed = 1;
429
- else failed = 1;
430
- }
431
- return { passed, failed };
503
+ return { passed, failed, success: exitCode === 0 };
432
504
  }
433
- function appendMetricsEvent(event) {
505
+ function recordMetricEvent(event) {
434
506
  try {
435
- let data = { events: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
436
- if (fs.existsSync(METRICS_FILE)) {
437
- const raw = fs.readFileSync(METRICS_FILE, "utf8");
507
+ let data = {};
508
+ if (fs3.existsSync(METRICS_FILE)) {
509
+ const raw = fs3.readFileSync(METRICS_FILE, "utf8");
438
510
  try {
439
511
  data = JSON.parse(raw);
440
512
  } catch {
@@ -444,7 +516,7 @@ function appendMetricsEvent(event) {
444
516
  data.events.push({ ...event, timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString() });
445
517
  data.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
446
518
  if (data.events.length > 500) data.events = data.events.slice(-400);
447
- fs.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
519
+ fs3.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
448
520
  } catch {
449
521
  }
450
522
  }
@@ -462,6 +534,396 @@ function extractFailuresFromOutput(runOutput) {
462
534
  }
463
535
  return failures.slice(0, 20);
464
536
  }
537
+ function generateFailureExplanation(testCode, runOutput, memory = {}) {
538
+ const lines = [];
539
+ lines.push("# An\xE1lise de Falha\n");
540
+ lines.push("## C\xF3digo do Teste");
541
+ lines.push("```");
542
+ lines.push(testCode.slice(0, 2e3));
543
+ lines.push("```\n");
544
+ lines.push("## Output da Execu\xE7\xE3o");
545
+ lines.push("```");
546
+ lines.push(runOutput.slice(0, 2e3));
547
+ lines.push("```\n");
548
+ if (memory.learnings && memory.learnings.length > 0) {
549
+ lines.push("## Aprendizados Anteriores (\xFAltimos 5)");
550
+ memory.learnings.slice(-5).forEach((l) => {
551
+ lines.push(`- **${l.type}**: ${l.description || "N/A"}`);
552
+ });
553
+ lines.push("");
554
+ }
555
+ lines.push("## Sua Tarefa");
556
+ lines.push("1. Identifique a causa raiz da falha");
557
+ lines.push("2. Sugira uma corre\xE7\xE3o espec\xEDfica");
558
+ lines.push("3. Explique por que essa corre\xE7\xE3o deve funcionar");
559
+ return lines.join("\n");
560
+ }
561
+
562
+ // src/cli/commands.js
563
+ import path4 from "path";
564
+ import fs4 from "fs";
565
+ import { spawn } from "child_process";
566
+ var PROJECT_ROOT4 = process.cwd();
567
+ var QA_AGENTS = {
568
+ autonomous: { desc: "Modo aut\xF4nomo: gera, testa, corrige e aprende", tools: ["qa_auto"] },
569
+ detection: { desc: "Detecta estrutura, frameworks, testes", tools: ["detect_project", "read_project", "list_test_files"] },
570
+ execution: { desc: "Executa testes, coverage, watch", tools: ["run_tests", "watch_tests", "get_test_coverage"] },
571
+ generation: { desc: "Gera e escreve testes", tools: ["generate_tests", "write_test", "create_test_template"] },
572
+ analysis: { desc: "Analisa falhas, sugere corre\xE7\xF5es", tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix", "analyze_file_methods"] },
573
+ browser: { desc: "Browser mode: screenshots, network, console", tools: ["web_eval_browser"] },
574
+ reporting: { desc: "Relat\xF3rios e m\xE9tricas", tools: ["create_bug_report", "get_business_metrics"] },
575
+ intelligence: { desc: "An\xE1lise preditiva e insights", tools: ["qa_full_analysis", "qa_health_check", "qa_suggest_next_test", "qa_predict_flaky", "qa_compare_with_industry", "qa_time_travel"] },
576
+ learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats"] },
577
+ maintenance: { desc: "Linter, deps, an\xE1lise de c\xF3digo", tools: ["run_linter", "install_dependencies"] }
578
+ };
579
+ function getExtensionAndBaseDir(fw, structure) {
580
+ const extMap = { cypress: ".cy.js", playwright: ".spec.js", jest: ".test.js", vitest: ".test.js", robot: ".robot", pytest: ".py" };
581
+ const ext = extMap[fw] || ".spec.js";
582
+ const baseDir = structure.testDirs[0] ? path4.join(PROJECT_ROOT4, structure.testDirs[0]) : path4.join(PROJECT_ROOT4, "tests");
583
+ return { ext, baseDir };
584
+ }
585
+ async function handleCLI() {
586
+ const cmd = process.argv[2];
587
+ if (cmd === "--help" || cmd === "-h") {
588
+ console.log(`
589
+ mcp-lab-agent - Executor + Consultor Inteligente de QA
590
+
591
+ USO:
592
+ mcp-lab-agent [comando] # Sem comando: inicia servidor MCP
593
+ mcp-lab-agent --help # Mostra esta ajuda
594
+
595
+ COMANDOS CLI:
596
+ analyze [NOVO] An\xE1lise completa: executa, analisa estabilidade, prev\xEA riscos e recomenda a\xE7\xF5es
597
+ auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
598
+ stats Mostra estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
599
+ detect [--json] Detecta frameworks e estrutura
600
+ route <tarefa> Sugere qual ferramenta usar
601
+ list Lista ferramentas MCP dispon\xEDveis
602
+
603
+ EXEMPLOS:
604
+ mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
605
+ mcp-lab-agent auto "login flow" --max-retries 5
606
+ mcp-lab-agent stats
607
+ mcp-lab-agent detect --json
608
+
609
+ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
610
+ Adicione ao ~/.cursor/mcp.json:
611
+ {
612
+ "mcpServers": {
613
+ "qa-lab-agent": {
614
+ "command": "npx",
615
+ "args": ["-y", "mcp-lab-agent"],
616
+ "cwd": "\${workspaceFolder}"
617
+ }
618
+ }
619
+ }
620
+
621
+ AMBIENTES CORPORATIVOS (APIs bloqueadas):
622
+ Use Ollama (100% offline):
623
+ brew install ollama
624
+ ollama pull llama3.1:8b
625
+ ollama serve
626
+ mcp-lab-agent analyze # Funciona sem internet!
627
+ `);
628
+ return true;
629
+ }
630
+ if (cmd === "detect") {
631
+ const structure = detectProjectStructure();
632
+ const jsonOnly = process.argv.includes("--json");
633
+ if (jsonOnly) {
634
+ console.log(JSON.stringify(structure, null, 2));
635
+ } else {
636
+ const lines = [
637
+ "",
638
+ "mcp-lab-agent \xB7 detec\xE7\xE3o",
639
+ "\u2500".repeat(40),
640
+ `Frameworks: ${structure.testFrameworks.length ? structure.testFrameworks.join(", ") : "nenhum"}`,
641
+ `Pastas: ${structure.testDirs.length ? structure.testDirs.join(", ") : "nenhuma"}`,
642
+ `Backend: ${structure.backendDir || "\u2014"}`,
643
+ `Frontend: ${structure.frontendDir || "\u2014"}`,
644
+ `Mobile: ${structure.hasMobile ? "sim" : "\u2014"}`,
645
+ "\u2500".repeat(40),
646
+ "(use --json para sa\xEDda completa)",
647
+ ""
648
+ ];
649
+ console.log(lines.join("\n"));
650
+ }
651
+ return true;
652
+ }
653
+ if (cmd === "list") {
654
+ const agents = Object.entries(QA_AGENTS).map(([k, v]) => ` ${k}: ${v.tools.join(", ")}`);
655
+ console.log("Agentes e ferramentas:\n" + agents.join("\n"));
656
+ return true;
657
+ }
658
+ if (cmd === "route" && process.argv[3]) {
659
+ const task = process.argv.slice(3).join(" ");
660
+ const t = task.toLowerCase();
661
+ let agent = "detection";
662
+ if (/autônomo|auto|completo|loop|aprende|corrige automaticamente/i.test(t)) agent = "autonomous";
663
+ else if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats/i.test(t)) agent = "learning";
664
+ else if (/rodar|executar|run|test|coverage|watch/i.test(t)) agent = "execution";
665
+ else if (/gerar|criar|escrever|generate|write|template/i.test(t)) agent = "generation";
666
+ else if (/analisar|por que|falhou|sugerir|fix|selector/i.test(t)) agent = "analysis";
667
+ else if (/browser|navegador|screenshot|network|console/i.test(t)) agent = "browser";
668
+ else if (/relatório|métrica|bug report/i.test(t)) agent = "reporting";
669
+ else if (/linter|dependência|instalar|analisar método/i.test(t)) agent = "maintenance";
670
+ const a = QA_AGENTS[agent] || QA_AGENTS.detection;
671
+ console.log(JSON.stringify({ suggestedAgent: agent, suggestedTools: a.tools, description: a.desc }, null, 2));
672
+ return true;
673
+ }
674
+ if (cmd === "auto") {
675
+ await handleAutoCommand();
676
+ return true;
677
+ }
678
+ if (cmd === "stats") {
679
+ const stats = getMemoryStats();
680
+ console.log(`
681
+ \u{1F4CA} Estat\xEDsticas de Aprendizado
682
+
683
+ Total de aprendizados: ${stats.totalLearnings}
684
+ Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
685
+ Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
686
+ Corre\xE7\xF5es de timing: ${stats.timingFixes}
687
+ Testes gerados: ${stats.testsGenerated}
688
+ Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
689
+
690
+ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Use 'mcp-lab-agent auto <descri\xE7\xE3o>' para gerar testes e aprender com erros." : ""}
691
+ `);
692
+ return true;
693
+ }
694
+ if (cmd === "analyze") {
695
+ await handleAnalyzeCommand();
696
+ return true;
697
+ }
698
+ return false;
699
+ }
700
+ async function handleAutoCommand() {
701
+ const request = process.argv.slice(3).join(" ");
702
+ if (!request) {
703
+ console.error("\u274C Uso: mcp-lab-agent auto <descri\xE7\xE3o do teste> [--max-retries N]");
704
+ process.exit(1);
705
+ }
706
+ const maxRetriesIdx = process.argv.indexOf("--max-retries");
707
+ const maxRetries = maxRetriesIdx !== -1 && process.argv[maxRetriesIdx + 1] ? parseInt(process.argv[maxRetriesIdx + 1], 10) : 3;
708
+ const cleanRequest = request.replace(/--max-retries\s+\d+/g, "").trim();
709
+ console.log(`
710
+ \u{1F916} Modo aut\xF4nomo iniciado: "${cleanRequest}"
711
+ `);
712
+ const structure = detectProjectStructure();
713
+ const fw = structure.testFrameworks[0];
714
+ if (!fw) {
715
+ console.error("\u274C Nenhum framework detectado.");
716
+ process.exit(1);
717
+ }
718
+ const llm = resolveLLMProvider("simple");
719
+ if (!llm.apiKey) {
720
+ console.error("\u274C Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env");
721
+ process.exit(1);
722
+ }
723
+ const memory = loadProjectMemory();
724
+ const contextLines = [
725
+ `Frameworks: ${structure.testFrameworks.join(", ")}`,
726
+ `Pastas: ${structure.testDirs.join(", ")}`,
727
+ memory.flows?.length ? `Fluxos: ${memory.flows.map((f) => f.name || f.id).join(", ")}` : ""
728
+ ].filter(Boolean).join("\n");
729
+ let testFilePath = null;
730
+ let testContent = null;
731
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
732
+ console.log(`
733
+ [Tentativa ${attempt}/${maxRetries}] Gerando teste...`);
734
+ const { provider, apiKey, baseUrl, model } = llm;
735
+ const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
736
+ const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
737
+ ${memoryHints ? `
738
+ Aprendizados anteriores (use como refer\xEAncia):
739
+ ${memoryHints.slice(0, 1e3)}` : ""}
740
+ Retorne SOMENTE o c\xF3digo, sem markdown.`;
741
+ const userPrompt = `Contexto:
742
+ ${contextLines}
743
+
744
+ Gere teste para: ${cleanRequest}
745
+ Framework: ${fw}`;
746
+ try {
747
+ let specContent = "";
748
+ if (provider === "gemini") {
749
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
750
+ const res = await fetch(url, {
751
+ method: "POST",
752
+ headers: { "Content-Type": "application/json" },
753
+ body: JSON.stringify({
754
+ contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
755
+ generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
756
+ })
757
+ });
758
+ const data = await res.json();
759
+ specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
760
+ } else {
761
+ const res = await fetch(`${baseUrl}/chat/completions`, {
762
+ method: "POST",
763
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
764
+ body: JSON.stringify({
765
+ model,
766
+ messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
767
+ temperature: 0.3,
768
+ max_tokens: 4096
769
+ })
770
+ });
771
+ const data = await res.json();
772
+ specContent = data.choices?.[0]?.message?.content || "";
773
+ }
774
+ specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
775
+ testContent = specContent;
776
+ if (!testFilePath) {
777
+ const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
778
+ const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
779
+ const safeName = fileName + ext;
780
+ testFilePath = path4.join(baseDir, safeName);
781
+ if (!fs4.existsSync(baseDir)) fs4.mkdirSync(baseDir, { recursive: true });
782
+ }
783
+ fs4.writeFileSync(testFilePath, testContent, "utf8");
784
+ console.log(`\u2705 Teste gravado: ${testFilePath}`);
785
+ console.log(`
786
+ [Tentativa ${attempt}/${maxRetries}] Executando teste...`);
787
+ const runResult = await new Promise((resolve) => {
788
+ const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
789
+ cwd: PROJECT_ROOT4,
790
+ stdio: ["inherit", "pipe", "pipe"],
791
+ shell: process.platform === "win32"
792
+ });
793
+ let stdout = "", stderr = "";
794
+ if (child.stdout) child.stdout.on("data", (d) => {
795
+ stdout += d.toString();
796
+ });
797
+ if (child.stderr) child.stderr.on("data", (d) => {
798
+ stderr += d.toString();
799
+ });
800
+ child.on("close", (code) => resolve({ code, output: [stdout, stderr].filter(Boolean).join("\n") }));
801
+ });
802
+ if (runResult.code === 0) {
803
+ console.log(`
804
+ \u2705 Teste passou na tentativa ${attempt}!`);
805
+ saveProjectMemory({
806
+ learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
807
+ });
808
+ console.log(`
809
+ \u{1F4CA} Aprendizado salvo. Use "mcp-lab-agent stats" para ver m\xE9tricas.
810
+ `);
811
+ process.exit(0);
812
+ }
813
+ console.log(`
814
+ \u274C Teste falhou (exit ${runResult.code})`);
815
+ console.log(`
816
+ Sa\xEDda:
817
+ ${runResult.output.slice(0, 800)}
818
+ `);
819
+ if (attempt >= maxRetries) {
820
+ console.log(`
821
+ \u274C Limite de tentativas atingido (${maxRetries}).
822
+ `);
823
+ saveProjectMemory({
824
+ learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
825
+ });
826
+ process.exit(1);
827
+ }
828
+ console.log(`
829
+ [Tentativa ${attempt}/${maxRetries}] Analisando falha...`);
830
+ const flakyAnalysis = detectFlakyPatterns(runResult.output);
831
+ if (flakyAnalysis.isLikelyFlaky) {
832
+ console.log(`\u26A0\uFE0F Flaky detectado (${flakyAnalysis.confidence.toFixed(2)}): ${flakyAnalysis.patterns.map((p) => p.pattern).join(", ")}`);
833
+ }
834
+ console.log(`
835
+ [Tentativa ${attempt}/${maxRetries}] Aplicando corre\xE7\xE3o (simulada)...`);
836
+ console.log(`\u26A0\uFE0F Corre\xE7\xE3o autom\xE1tica ainda n\xE3o implementada nesta vers\xE3o CLI. Tentando novamente...`);
837
+ } catch (err) {
838
+ console.error(`
839
+ \u274C Erro na tentativa ${attempt}: ${err.message}
840
+ `);
841
+ process.exit(1);
842
+ }
843
+ }
844
+ console.log(`
845
+ \u274C Falhou ap\xF3s ${maxRetries} tentativa(s).
846
+ `);
847
+ process.exit(1);
848
+ }
849
+ async function handleAnalyzeCommand() {
850
+ console.log("\n\u{1F916} An\xE1lise completa iniciada...\n");
851
+ const structure = detectProjectStructure();
852
+ console.log("[1/4] \u{1F50D} Detectando estrutura...");
853
+ console.log(`\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
854
+ `);
855
+ const testFiles = structure.testDirs.flatMap((dir) => {
856
+ const fullPath = path4.join(PROJECT_ROOT4, dir);
857
+ if (!fs4.existsSync(fullPath)) return [];
858
+ return fs4.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
859
+ });
860
+ console.log(`\u2705 ${testFiles.length} teste(s) encontrado(s)
861
+ `);
862
+ console.log("[2/4] \u{1F9E0} Analisando estabilidade...");
863
+ const stabilityAnalysis = analyzeTestStability();
864
+ const unstableTests = stabilityAnalysis.tests.filter((t) => t.failureRate > 20);
865
+ if (unstableTests.length > 0) {
866
+ console.log(`\u26A0\uFE0F ${unstableTests.length} teste(s) inst\xE1vel(is):`);
867
+ unstableTests.slice(0, 3).forEach((t) => {
868
+ console.log(` - ${t.file}: ${t.failureRate}% de falha (${t.failed}/${t.total} execu\xE7\xF5es)`);
869
+ });
870
+ } else {
871
+ console.log("\u2705 Todos os testes s\xE3o est\xE1veis");
872
+ }
873
+ console.log();
874
+ console.log("[3/4] \u{1F52E} Analisando riscos por \xE1rea...");
875
+ const codeRisks = analyzeCodeRisks();
876
+ const highRisks = codeRisks.filter((r) => r.risk === "high");
877
+ if (highRisks.length > 0) {
878
+ console.log(`\u{1F534} ${highRisks.length} \xE1rea(s) de RISCO ALTO:`);
879
+ highRisks.slice(0, 3).forEach((r) => {
880
+ console.log(` - ${r.area}/: ${r.files} arquivo(s) sem testes`);
881
+ });
882
+ } else {
883
+ console.log("\u2705 Todas as \xE1reas principais t\xEAm cobertura");
884
+ }
885
+ console.log();
886
+ console.log("[4/4] \u{1F4A1} Gerando recomenda\xE7\xF5es...\n");
887
+ const actions = [];
888
+ unstableTests.forEach((t) => {
889
+ actions.push({ priority: "\u{1F534} URGENTE", action: `Refatore ${t.file} (falha ${t.failureRate}%)`, command: `mcp-lab-agent auto "corrigir ${t.file}"` });
890
+ });
891
+ highRisks.forEach((r) => {
892
+ actions.push({ priority: "\u{1F534} URGENTE", action: `Adicione testes para ${r.area}/`, command: `mcp-lab-agent auto "testes para ${r.area}"` });
893
+ });
894
+ let score = 100;
895
+ score -= unstableTests.length * 10;
896
+ score -= highRisks.length * 15;
897
+ score = Math.max(0, score);
898
+ const emoji = score >= 80 ? "\u{1F680}" : score >= 60 ? "\u2705" : "\u26A0\uFE0F";
899
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
900
+ console.log(`${emoji} RELAT\xD3RIO COMPLETO
901
+ `);
902
+ console.log(`Nota: ${score}/100
903
+ `);
904
+ console.log("A\xC7\xD5ES RECOMENDADAS:\n");
905
+ actions.slice(0, 5).forEach((a, i) => {
906
+ console.log(`${i + 1}. ${a.priority}: ${a.action}`);
907
+ console.log(` \u2192 ${a.command}
908
+ `);
909
+ });
910
+ if (actions.length === 0) {
911
+ console.log("\u2705 Projeto em excelente estado!\n");
912
+ }
913
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
914
+ }
915
+
916
+ // src/index.js
917
+ var PROJECT_ROOT5 = process.cwd();
918
+ config({ path: path5.join(PROJECT_ROOT5, ".env") });
919
+ var server = new McpServer({
920
+ name: "mcp-lab-agent",
921
+ version: "2.1.0"
922
+ });
923
+ var METRICS_FILE2 = path5.join(PROJECT_ROOT5, ".qa-lab-metrics.json");
924
+ function appendMetricsEvent(event) {
925
+ recordMetricEvent(event);
926
+ }
465
927
  server.registerTool(
466
928
  "read_file",
467
929
  {
@@ -479,20 +941,20 @@ server.registerTool(
479
941
  },
480
942
  async ({ path: filePath, encoding = "utf8" }) => {
481
943
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
482
- const fullPath = path.join(PROJECT_ROOT, normalized);
483
- if (!fullPath.startsWith(PROJECT_ROOT)) {
944
+ const fullPath = path5.join(PROJECT_ROOT5, normalized);
945
+ if (!fullPath.startsWith(PROJECT_ROOT5)) {
484
946
  return {
485
947
  content: [{ type: "text", text: "Caminho fora do projeto." }],
486
948
  structuredContent: { ok: false, error: "Path outside project" }
487
949
  };
488
950
  }
489
- if (!fs.existsSync(fullPath)) {
951
+ if (!fs5.existsSync(fullPath)) {
490
952
  return {
491
953
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
492
954
  structuredContent: { ok: false, error: "File not found" }
493
955
  };
494
956
  }
495
- const stat = fs.statSync(fullPath);
957
+ const stat = fs5.statSync(fullPath);
496
958
  if (stat.isDirectory()) {
497
959
  return {
498
960
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Use um caminho de arquivo." }],
@@ -500,7 +962,7 @@ server.registerTool(
500
962
  };
501
963
  }
502
964
  try {
503
- const content = fs.readFileSync(fullPath, encoding);
965
+ const content = fs5.readFileSync(fullPath, encoding);
504
966
  return {
505
967
  content: [{ type: "text", text: content }],
506
968
  structuredContent: { ok: true, content }
@@ -579,7 +1041,7 @@ server.registerTool(
579
1041
  structuredContent: { ok: false, error: "Playwright not installed. Run: npm install playwright" }
580
1042
  };
581
1043
  }
582
- const outPath = screenshotPath ? path.join(PROJECT_ROOT, screenshotPath.replace(/^\//, "")) : path.join(PROJECT_ROOT, ".qa-lab-screenshot.png");
1044
+ const outPath = screenshotPath ? path5.join(PROJECT_ROOT5, screenshotPath.replace(/^\//, "")) : path5.join(PROJECT_ROOT5, ".qa-lab-screenshot.png");
583
1045
  const consoleLogs = [];
584
1046
  const consoleErrors = [];
585
1047
  const networkRequests = [];
@@ -606,7 +1068,7 @@ server.registerTool(
606
1068
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
607
1069
  await page.screenshot({ path: outPath, fullPage: false });
608
1070
  await browser.close();
609
- const relPath = path.relative(PROJECT_ROOT, outPath);
1071
+ const relPath = path5.relative(PROJECT_ROOT5, outPath);
610
1072
  let summary = `Screenshot salvo: ${relPath}`;
611
1073
  if (consoleErrors.length) summary += `
612
1074
 
@@ -633,15 +1095,16 @@ Requisi\xE7\xF5es: ${networkRequests.length}`;
633
1095
  }
634
1096
  }
635
1097
  );
636
- var QA_AGENTS = {
1098
+ var QA_AGENTS2 = {
637
1099
  autonomous: { tools: ["qa_auto"], desc: "Modo aut\xF4nomo: gera, roda, corrige e aprende (loop completo)" },
1100
+ intelligence: { tools: ["qa_full_analysis", "qa_health_check", "qa_suggest_next_test", "qa_predict_flaky", "qa_compare_with_industry"], desc: "Executor + Consultor: an\xE1lise completa, diagn\xF3stico, sugest\xF5es e predi\xE7\xF5es" },
638
1101
  detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
639
1102
  execution: { tools: ["run_tests", "watch_tests", "get_test_coverage"], desc: "Execu\xE7\xE3o de testes e cobertura" },
640
1103
  generation: { tools: ["generate_tests", "write_test", "create_test_template"], desc: "Gera\xE7\xE3o de testes com LLM" },
641
1104
  analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
642
1105
  browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
643
1106
  reporting: { tools: ["create_bug_report", "get_business_metrics"], desc: "Relat\xF3rios e m\xE9tricas" },
644
- learning: { tools: ["qa_learning_stats"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
1107
+ learning: { tools: ["qa_learning_stats", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
645
1108
  maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
646
1109
  };
647
1110
  server.registerTool(
@@ -662,30 +1125,33 @@ server.registerTool(
662
1125
  async ({ task }) => {
663
1126
  const t = task.toLowerCase();
664
1127
  if (/autônomo|auto|completo|loop|aprende|corrige automaticamente/i.test(t)) {
665
- return { content: [{ type: "text", text: "Agente: autonomous \u2192 qa_auto (loop completo: gera, roda, corrige, aprende)" }], structuredContent: { ok: true, suggestedAgent: "autonomous", suggestedTools: QA_AGENTS.autonomous.tools, description: QA_AGENTS.autonomous.desc } };
1128
+ return { content: [{ type: "text", text: "Agente: autonomous \u2192 qa_auto (loop completo: gera, roda, corrige, aprende)" }], structuredContent: { ok: true, suggestedAgent: "autonomous", suggestedTools: QA_AGENTS2.autonomous.tools, description: QA_AGENTS2.autonomous.desc } };
1129
+ }
1130
+ if (/health|saúde|diagnóstico|nota|score|próximo teste|sugerir|prever|flaky|benchmark|comparar|indústria/i.test(t)) {
1131
+ return { content: [{ type: "text", text: "Agente: intelligence \u2192 qa_health_check, qa_suggest_next_test, qa_predict_flaky, qa_compare_with_industry" }], structuredContent: { ok: true, suggestedAgent: "intelligence", suggestedTools: QA_AGENTS2.intelligence.tools, description: QA_AGENTS2.intelligence.desc } };
666
1132
  }
667
- if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats/i.test(t)) {
668
- return { content: [{ type: "text", text: "Agente: learning \u2192 qa_learning_stats" }], structuredContent: { ok: true, suggestedAgent: "learning", suggestedTools: QA_AGENTS.learning.tools, description: QA_AGENTS.learning.desc } };
1133
+ if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats|evolução|timeline|tempo|histórico/i.test(t)) {
1134
+ return { content: [{ type: "text", text: "Agente: learning \u2192 qa_learning_stats, qa_time_travel" }], structuredContent: { ok: true, suggestedAgent: "learning", suggestedTools: QA_AGENTS2.learning.tools, description: QA_AGENTS2.learning.desc } };
669
1135
  }
670
1136
  if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
671
- return { content: [{ type: "text", text: "Agente: execution \u2192 run_tests, get_test_coverage" }], structuredContent: { ok: true, suggestedAgent: "execution", suggestedTools: QA_AGENTS.execution.tools, description: QA_AGENTS.execution.desc } };
1137
+ return { content: [{ type: "text", text: "Agente: execution \u2192 run_tests, get_test_coverage" }], structuredContent: { ok: true, suggestedAgent: "execution", suggestedTools: QA_AGENTS2.execution.tools, description: QA_AGENTS2.execution.desc } };
672
1138
  }
673
1139
  if (/gerar|criar|escrever|generate|write|template/i.test(t)) {
674
- return { content: [{ type: "text", text: "Agente: generation \u2192 generate_tests, write_test" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: QA_AGENTS.generation.tools, description: QA_AGENTS.generation.desc } };
1140
+ return { content: [{ type: "text", text: "Agente: generation \u2192 generate_tests, write_test" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: QA_AGENTS2.generation.tools, description: QA_AGENTS2.generation.desc } };
675
1141
  }
676
1142
  if (/analisar|por que|falhou|suggest|correção|selector|fix/i.test(t)) {
677
- return { content: [{ type: "text", text: "Agente: analysis \u2192 analyze_failures, por_que_falhou, suggest_fix" }], structuredContent: { ok: true, suggestedAgent: "analysis", suggestedTools: QA_AGENTS.analysis.tools, description: QA_AGENTS.analysis.desc } };
1143
+ return { content: [{ type: "text", text: "Agente: analysis \u2192 analyze_failures, por_que_falhou, suggest_fix" }], structuredContent: { ok: true, suggestedAgent: "analysis", suggestedTools: QA_AGENTS2.analysis.tools, description: QA_AGENTS2.analysis.desc } };
678
1144
  }
679
1145
  if (/browser|screenshot|navegador|avaliar|ux|network|console/i.test(t)) {
680
- return { content: [{ type: "text", text: "Agente: browser \u2192 web_eval_browser" }], structuredContent: { ok: true, suggestedAgent: "browser", suggestedTools: QA_AGENTS.browser.tools, description: QA_AGENTS.browser.desc } };
1146
+ return { content: [{ type: "text", text: "Agente: browser \u2192 web_eval_browser" }], structuredContent: { ok: true, suggestedAgent: "browser", suggestedTools: QA_AGENTS2.browser.tools, description: QA_AGENTS2.browser.desc } };
681
1147
  }
682
1148
  if (/detectar|estrutura|listar|arquivos|framework/i.test(t)) {
683
- return { content: [{ type: "text", text: "Agente: detection \u2192 detect_project, list_test_files" }], structuredContent: { ok: true, suggestedAgent: "detection", suggestedTools: QA_AGENTS.detection.tools, description: QA_AGENTS.detection.desc } };
1149
+ return { content: [{ type: "text", text: "Agente: detection \u2192 detect_project, list_test_files" }], structuredContent: { ok: true, suggestedAgent: "detection", suggestedTools: QA_AGENTS2.detection.tools, description: QA_AGENTS2.detection.desc } };
684
1150
  }
685
- if (/relatório|bug|métricas|metrics|coverage/i.test(t)) {
686
- return { content: [{ type: "text", text: "Agente: reporting \u2192 create_bug_report, get_business_metrics" }], structuredContent: { ok: true, suggestedAgent: "reporting", suggestedTools: QA_AGENTS.reporting.tools, description: QA_AGENTS.reporting.desc } };
1151
+ if (/relatório|bug|métricas|metrics/i.test(t)) {
1152
+ return { content: [{ type: "text", text: "Agente: reporting \u2192 create_bug_report, get_business_metrics" }], structuredContent: { ok: true, suggestedAgent: "reporting", suggestedTools: QA_AGENTS2.reporting.tools, description: QA_AGENTS2.reporting.desc } };
687
1153
  }
688
- return { content: [{ type: "text", text: "Agente: detection (gen\xE9rico)" }], structuredContent: { ok: true, suggestedAgent: "detection", suggestedTools: QA_AGENTS.detection.tools, description: QA_AGENTS.detection.desc } };
1154
+ return { content: [{ type: "text", text: "Agente: detection (gen\xE9rico)" }], structuredContent: { ok: true, suggestedAgent: "detection", suggestedTools: QA_AGENTS2.detection.tools, description: QA_AGENTS2.detection.desc } };
689
1155
  }
690
1156
  );
691
1157
  server.registerTool(
@@ -744,11 +1210,11 @@ server.registerTool(
744
1210
  if (selectedFramework === "cypress") {
745
1211
  cmd = "npx";
746
1212
  args = spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"];
747
- cwd = structure.testDirs.includes("cypress") ? path.join(PROJECT_ROOT, "cypress") : structure.testDirs[0] ? path.join(PROJECT_ROOT, structure.testDirs[0]) : PROJECT_ROOT;
1213
+ cwd = structure.testDirs.includes("cypress") ? path5.join(PROJECT_ROOT5, "cypress") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5;
748
1214
  } else if (selectedFramework === "playwright") {
749
1215
  cmd = "npx";
750
1216
  args = spec ? ["playwright", "test", spec] : ["playwright", "test"];
751
- cwd = structure.testDirs.includes("playwright") ? path.join(PROJECT_ROOT, "playwright") : structure.testDirs[0] ? path.join(PROJECT_ROOT, structure.testDirs[0]) : PROJECT_ROOT;
1217
+ cwd = structure.testDirs.includes("playwright") ? path5.join(PROJECT_ROOT5, "playwright") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5;
752
1218
  } else if (selectedFramework === "webdriverio") {
753
1219
  cmd = "npx";
754
1220
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
@@ -773,45 +1239,45 @@ server.registerTool(
773
1239
  cmd = "npx";
774
1240
  args = ["jest"];
775
1241
  if (spec) args.push(spec);
776
- cwd = PROJECT_ROOT;
1242
+ cwd = PROJECT_ROOT5;
777
1243
  } else if (selectedFramework === "vitest") {
778
1244
  cmd = "npx";
779
1245
  args = ["vitest", "run"];
780
1246
  if (spec) args.push(spec);
781
- cwd = PROJECT_ROOT;
1247
+ cwd = PROJECT_ROOT5;
782
1248
  } else if (selectedFramework === "mocha") {
783
1249
  cmd = "npx";
784
1250
  args = spec ? ["mocha", spec] : ["mocha"];
785
- cwd = PROJECT_ROOT;
1251
+ cwd = PROJECT_ROOT5;
786
1252
  } else if (selectedFramework === "appium") {
787
1253
  cmd = "npx";
788
1254
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
789
- cwd = PROJECT_ROOT;
1255
+ cwd = PROJECT_ROOT5;
790
1256
  } else if (selectedFramework === "detox") {
791
1257
  cmd = "npx";
792
1258
  args = ["detox", "test"];
793
1259
  if (spec) args.push(spec);
794
- cwd = PROJECT_ROOT;
1260
+ cwd = PROJECT_ROOT5;
795
1261
  } else if (selectedFramework === "robot") {
796
1262
  cmd = "robot";
797
1263
  args = spec ? [spec] : [structure.testDirs[0] || "tests"];
798
- cwd = PROJECT_ROOT;
1264
+ cwd = PROJECT_ROOT5;
799
1265
  } else if (selectedFramework === "pytest") {
800
1266
  cmd = "pytest";
801
1267
  args = spec ? [spec] : [];
802
- cwd = PROJECT_ROOT;
1268
+ cwd = PROJECT_ROOT5;
803
1269
  } else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
804
1270
  cmd = "npm";
805
1271
  args = ["test"];
806
- cwd = PROJECT_ROOT;
1272
+ cwd = PROJECT_ROOT5;
807
1273
  } else {
808
1274
  cmd = "npm";
809
1275
  args = ["test"];
810
- cwd = PROJECT_ROOT;
1276
+ cwd = PROJECT_ROOT5;
811
1277
  }
812
1278
  const startTime = Date.now();
813
1279
  return new Promise((resolve) => {
814
- const child = spawn(cmd, args, {
1280
+ const child = spawn2(cmd, args, {
815
1281
  cwd,
816
1282
  stdio: ["inherit", "pipe", "pipe"],
817
1283
  shell: process.platform === "win32",
@@ -839,7 +1305,7 @@ server.registerTool(
839
1305
  const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
840
1306
  if (!passed && runOutput) {
841
1307
  try {
842
- fs.writeFileSync(path.join(PROJECT_ROOT, ".qa-lab-last-failure.log"), runOutput, "utf8");
1308
+ fs5.writeFileSync(path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log"), runOutput, "utf8");
843
1309
  } catch {
844
1310
  }
845
1311
  }
@@ -855,6 +1321,15 @@ server.registerTool(
855
1321
  failures: !passed ? extractFailuresFromOutput(runOutput) : void 0
856
1322
  });
857
1323
  if (passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
1324
+ saveProjectMemory({
1325
+ execution: {
1326
+ testFile: spec || "all",
1327
+ passed,
1328
+ duration: durationSeconds,
1329
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1330
+ framework: selectedFramework
1331
+ }
1332
+ });
858
1333
  resolve({
859
1334
  content: [{ type: "text", text: passed ? "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes." }],
860
1335
  structuredContent: {
@@ -954,10 +1429,10 @@ server.registerTool(
954
1429
  ${referenceCode.slice(0, 8e3)}`;
955
1430
  if (referencePaths?.length) {
956
1431
  for (const p of referencePaths.slice(0, 5)) {
957
- const full = path.join(PROJECT_ROOT, p.replace(/^\//, "").replace(/\\/g, "/"));
958
- if (fs.existsSync(full)) {
1432
+ const full = path5.join(PROJECT_ROOT5, p.replace(/^\//, "").replace(/\\/g, "/"));
1433
+ if (fs5.existsSync(full)) {
959
1434
  try {
960
- const content = fs.readFileSync(full, "utf8");
1435
+ const content = fs5.readFileSync(full, "utf8");
961
1436
  referenceBlock += `
962
1437
 
963
1438
  --- Arquivo: ${p} ---
@@ -1054,7 +1529,7 @@ Framework alvo: ${fw}${referenceBlock}`;
1054
1529
  }
1055
1530
  }
1056
1531
  );
1057
- function getExtensionAndBaseDir(fw, structure) {
1532
+ function getExtensionAndBaseDir2(fw, structure) {
1058
1533
  const extMap = {
1059
1534
  cypress: ".cy.js",
1060
1535
  playwright: ".spec.js",
@@ -1079,7 +1554,7 @@ function getExtensionAndBaseDir(fw, structure) {
1079
1554
  robot: structure.testDirs.includes("robot") ? "robot" : structure.testDirs[0] || "tests",
1080
1555
  behave: structure.testDirs.includes("features") ? "features" : structure.testDirs[0] || "tests"
1081
1556
  };
1082
- const baseDir = path.join(PROJECT_ROOT, baseMap[fw] || structure.testDirs[0] || "tests");
1557
+ const baseDir = path5.join(PROJECT_ROOT5, baseMap[fw] || structure.testDirs[0] || "tests");
1083
1558
  return { ext, baseDir };
1084
1559
  }
1085
1560
  server.registerTool(
@@ -1121,16 +1596,16 @@ server.registerTool(
1121
1596
  structuredContent: { ok: false, error: "No test framework" }
1122
1597
  };
1123
1598
  }
1124
- const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
1599
+ const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
1125
1600
  const safeName = name.replace(/[^a-z0-9-_]/gi, "-").replace(/-+/g, "-").replace(/_+/g, "_").replace(/\.(cy|spec|test|robot|feature|py)\.?(js|ts|py)?$/i, "").replace(/^[-_]+|[-_]+$/g, "");
1126
1601
  const fileName = ext.startsWith("_") ? `${safeName}${ext}` : `${safeName}${ext}`;
1127
- const targetDir = subdir ? path.join(baseDir, subdir) : baseDir;
1128
- const filePath = path.join(targetDir, fileName);
1602
+ const targetDir = subdir ? path5.join(baseDir, subdir) : baseDir;
1603
+ const filePath = path5.join(targetDir, fileName);
1129
1604
  try {
1130
- if (!fs.existsSync(targetDir)) {
1131
- fs.mkdirSync(targetDir, { recursive: true });
1605
+ if (!fs5.existsSync(targetDir)) {
1606
+ fs5.mkdirSync(targetDir, { recursive: true });
1132
1607
  }
1133
- fs.writeFileSync(filePath, content, "utf8");
1608
+ fs5.writeFileSync(filePath, content, "utf8");
1134
1609
  return {
1135
1610
  content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
1136
1611
  structuredContent: { ok: true, path: filePath }
@@ -1255,80 +1730,6 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
1255
1730
  const data = await res.json();
1256
1731
  return data.choices?.[0]?.message?.content || "";
1257
1732
  }
1258
- async function generateFailureExplanation(resolvedOutput, testFilePath = null) {
1259
- const structure = detectProjectStructure();
1260
- const fw = structure.testFrameworks[0] || "unknown";
1261
- let testCode = "";
1262
- if (testFilePath) {
1263
- const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
1264
- const fullPath = path.join(PROJECT_ROOT, normalized);
1265
- if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) {
1266
- try {
1267
- testCode = fs.readFileSync(fullPath, "utf8");
1268
- } catch {
1269
- }
1270
- }
1271
- }
1272
- const llm = resolveLLMProvider("complex");
1273
- if (!llm.apiKey) {
1274
- return { ok: false, error: "No API key", formattedText: null };
1275
- }
1276
- const { provider, apiKey, baseUrl, model } = llm;
1277
- const fwHints = {
1278
- webdriverio: "WebdriverIO (describe/it, $, browser.$)",
1279
- appium: "Appium/WebdriverIO (mobile, $, browser.$)",
1280
- playwright: "Playwright (test, page, locator)",
1281
- cypress: "Cypress (cy.get, cy.click)",
1282
- jest: "Jest (describe, test, expect)",
1283
- vitest: "Vitest (describe, test, expect)",
1284
- robot: "Robot Framework",
1285
- pytest: "pytest"
1286
- };
1287
- const systemPrompt = `Voc\xEA \xE9 um mentor de QA. Analise o output de falha e responda em JSON (apenas o JSON, sem markdown) com as chaves:
1288
- - oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
1289
- - porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas, uma por item)
1290
- - oQueFazerAgora: array de strings (passos numerados do que fazer)
1291
- - sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o se aplic\xE1vel, no formato do framework)
1292
- - conceito: string ou null (ex: "Flaky test = teste intermitente. Geralmente por timing ou seletores fr\xE1geis.")
1293
- - framework: string (framework do projeto)
1294
-
1295
- Framework do projeto: ${fw}. ${fwHints[fw] || ""}
1296
- Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
1297
- const userPrompt = `Output do terminal/log (teste falhou):
1298
- ---
1299
- ${resolvedOutput.slice(0, 12e3)}
1300
- ---
1301
- ${testCode ? `
1302
- C\xF3digo do teste que falhou:
1303
- ---
1304
- ${testCode.slice(0, 6e3)}
1305
- ---` : ""}`;
1306
- try {
1307
- let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
1308
- raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
1309
- let data = {};
1310
- try {
1311
- data = JSON.parse(raw);
1312
- } catch {
1313
- data = {
1314
- oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear a resposta.",
1315
- porQueProvavelmenteFalhou: [],
1316
- oQueFazerAgora: [],
1317
- sugestaoCorrecao: null,
1318
- conceito: null,
1319
- framework: fw
1320
- };
1321
- }
1322
- data.framework = data.framework || fw;
1323
- if (testFilePath && data.sugestaoCorrecao) {
1324
- saveProjectMemory({ patterns: { [testFilePath]: { lastFix: data.sugestaoCorrecao?.slice(0, 500) } } });
1325
- }
1326
- const formattedText = formatFailureExplanation(data);
1327
- return { ok: true, formattedText, structuredContent: data };
1328
- } catch (err) {
1329
- return { ok: false, error: err.message, formattedText: null };
1330
- }
1331
- }
1332
1733
  server.registerTool(
1333
1734
  "por_que_falhou",
1334
1735
  {
@@ -1355,10 +1756,10 @@ server.registerTool(
1355
1756
  const fw = structure.testFrameworks[0] || "unknown";
1356
1757
  let resolvedOutput = errorOutput?.trim() || "";
1357
1758
  if (!resolvedOutput) {
1358
- const lastFailurePath = path.join(PROJECT_ROOT, ".qa-lab-last-failure.log");
1359
- if (fs.existsSync(lastFailurePath)) {
1759
+ const lastFailurePath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1760
+ if (fs5.existsSync(lastFailurePath)) {
1360
1761
  try {
1361
- resolvedOutput = fs.readFileSync(lastFailurePath, "utf8");
1762
+ resolvedOutput = fs5.readFileSync(lastFailurePath, "utf8");
1362
1763
  } catch {
1363
1764
  }
1364
1765
  }
@@ -1375,10 +1776,10 @@ server.registerTool(
1375
1776
  let testCode = "";
1376
1777
  if (testFilePath) {
1377
1778
  const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
1378
- const fullPath = path.join(PROJECT_ROOT, normalized);
1379
- if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) {
1779
+ const fullPath = path5.join(PROJECT_ROOT5, normalized);
1780
+ if (fs5.existsSync(fullPath) && !fs5.statSync(fullPath).isDirectory()) {
1380
1781
  try {
1381
- testCode = fs.readFileSync(fullPath, "utf8");
1782
+ testCode = fs5.readFileSync(fullPath, "utf8");
1382
1783
  } catch {
1383
1784
  }
1384
1785
  }
@@ -1542,9 +1943,9 @@ server.registerTool(
1542
1943
  const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
1543
1944
  let resolvedOutput = errorOutput;
1544
1945
  if (!resolvedOutput) {
1545
- const logPath = path.join(PROJECT_ROOT, ".qa-lab-last-failure.log");
1546
- if (fs.existsSync(logPath)) {
1547
- resolvedOutput = fs.readFileSync(logPath, "utf8");
1946
+ const logPath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1947
+ if (fs5.existsSync(logPath)) {
1948
+ resolvedOutput = fs5.readFileSync(logPath, "utf8");
1548
1949
  }
1549
1950
  }
1550
1951
  if (!resolvedOutput) {
@@ -1560,10 +1961,10 @@ server.registerTool(
1560
1961
  };
1561
1962
  }
1562
1963
  let testCode = "";
1563
- const fullPath = path.join(PROJECT_ROOT, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
1564
- if (fs.existsSync(fullPath)) {
1964
+ const fullPath = path5.join(PROJECT_ROOT5, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
1965
+ if (fs5.existsSync(fullPath)) {
1565
1966
  try {
1566
- testCode = fs.readFileSync(fullPath, "utf8");
1967
+ testCode = fs5.readFileSync(fullPath, "utf8");
1567
1968
  } catch {
1568
1969
  }
1569
1970
  }
@@ -1668,20 +2069,20 @@ server.registerTool(
1668
2069
  },
1669
2070
  async ({ path: filePath }) => {
1670
2071
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
1671
- const fullPath = path.join(PROJECT_ROOT, normalized);
1672
- if (!fullPath.startsWith(PROJECT_ROOT)) {
2072
+ const fullPath = path5.join(PROJECT_ROOT5, normalized);
2073
+ if (!fullPath.startsWith(PROJECT_ROOT5)) {
1673
2074
  return {
1674
2075
  content: [{ type: "text", text: "Caminho fora do projeto." }],
1675
2076
  structuredContent: { ok: false, error: "Path outside project" }
1676
2077
  };
1677
2078
  }
1678
- if (!fs.existsSync(fullPath)) {
2079
+ if (!fs5.existsSync(fullPath)) {
1679
2080
  return {
1680
2081
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
1681
2082
  structuredContent: { ok: false, error: "File not found" }
1682
2083
  };
1683
2084
  }
1684
- const stat = fs.statSync(fullPath);
2085
+ const stat = fs5.statSync(fullPath);
1685
2086
  if (stat.isDirectory()) {
1686
2087
  return {
1687
2088
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Informe um arquivo." }],
@@ -1690,7 +2091,7 @@ server.registerTool(
1690
2091
  }
1691
2092
  let fileContent = "";
1692
2093
  try {
1693
- fileContent = fs.readFileSync(fullPath, "utf8");
2094
+ fileContent = fs5.readFileSync(fullPath, "utf8");
1694
2095
  } catch (err) {
1695
2096
  return {
1696
2097
  content: [{ type: "text", text: `Erro ao ler: ${err.message}` }],
@@ -1708,7 +2109,7 @@ server.registerTool(
1708
2109
  };
1709
2110
  }
1710
2111
  const { provider, apiKey, baseUrl, model } = llm;
1711
- const ext = path.extname(fullPath).toLowerCase();
2112
+ const ext = path5.extname(fullPath).toLowerCase();
1712
2113
  const lang = [".ts", ".tsx"].includes(ext) ? "TypeScript" : [".js", ".jsx"].includes(ext) ? "JavaScript" : [".py"].includes(ext) ? "Python" : "c\xF3digo";
1713
2114
  const systemPrompt = `Voc\xEA \xE9 um revisor de c\xF3digo experiente em QA e testes. Analise o arquivo e cada m\xE9todo/fun\xE7\xE3o, respondendo em JSON v\xE1lido (sem markdown) com a estrutura:
1714
2115
 
@@ -1900,9 +2301,9 @@ server.registerTool(
1900
2301
  const msByPeriod = { "7d": 7 * 24 * 60 * 60 * 1e3, "30d": 30 * 24 * 60 * 60 * 1e3, all: Infinity };
1901
2302
  const cutoff = now - msByPeriod[period];
1902
2303
  let data = { events: [] };
1903
- if (fs.existsSync(METRICS_FILE)) {
2304
+ if (fs5.existsSync(METRICS_FILE2)) {
1904
2305
  try {
1905
- data = JSON.parse(fs.readFileSync(METRICS_FILE, "utf8"));
2306
+ data = JSON.parse(fs5.readFileSync(METRICS_FILE2, "utf8"));
1906
2307
  } catch {
1907
2308
  }
1908
2309
  }
@@ -1939,9 +2340,9 @@ server.registerTool(
1939
2340
  };
1940
2341
  }
1941
2342
  let flowCoverage = null;
1942
- if (fs.existsSync(FLOWS_CONFIG_FILE)) {
2343
+ if (fs5.existsSync(FLOWS_CONFIG_FILE)) {
1943
2344
  try {
1944
- const flowsConfig = JSON.parse(fs.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
2345
+ const flowsConfig = JSON.parse(fs5.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
1945
2346
  const flows = flowsConfig.flows || [];
1946
2347
  const structure = detectProjectStructure();
1947
2348
  const allTestFiles = new Set(collectTestFiles(structure).map((e) => e.path));
@@ -2079,8 +2480,8 @@ server.registerTool(
2079
2480
  };
2080
2481
  }
2081
2482
  return new Promise((resolve) => {
2082
- const child = spawn(cmd, args, {
2083
- cwd: PROJECT_ROOT,
2483
+ const child = spawn2(cmd, args, {
2484
+ cwd: PROJECT_ROOT5,
2084
2485
  stdio: ["inherit", "pipe", "pipe"],
2085
2486
  shell: process.platform === "win32",
2086
2487
  env: { ...process.env }
@@ -2126,13 +2527,13 @@ server.registerTool(
2126
2527
  async ({ packageManager = "auto" }) => {
2127
2528
  let pm = packageManager;
2128
2529
  if (pm === "auto") {
2129
- if (fs.existsSync(path.join(PROJECT_ROOT, "yarn.lock"))) pm = "yarn";
2130
- else if (fs.existsSync(path.join(PROJECT_ROOT, "pnpm-lock.yaml"))) pm = "pnpm";
2530
+ if (fs5.existsSync(path5.join(PROJECT_ROOT5, "yarn.lock"))) pm = "yarn";
2531
+ else if (fs5.existsSync(path5.join(PROJECT_ROOT5, "pnpm-lock.yaml"))) pm = "pnpm";
2131
2532
  else pm = "npm";
2132
2533
  }
2133
2534
  return new Promise((resolve) => {
2134
- const child = spawn(pm, ["install"], {
2135
- cwd: PROJECT_ROOT,
2535
+ const child = spawn2(pm, ["install"], {
2536
+ cwd: PROJECT_ROOT5,
2136
2537
  stdio: "inherit",
2137
2538
  shell: process.platform === "win32",
2138
2539
  env: { ...process.env }
@@ -2151,6 +2552,428 @@ server.registerTool(
2151
2552
  });
2152
2553
  }
2153
2554
  );
2555
+ server.registerTool(
2556
+ "qa_full_analysis",
2557
+ {
2558
+ title: "An\xE1lise completa: executor + consultor inteligente",
2559
+ description: "[EXECUTOR + CONSULTOR] An\xE1lise completa em 1 comando: detecta, executa testes, analisa estabilidade, prev\xEA problemas, calcula riscos por \xE1rea e gera recomenda\xE7\xF5es acion\xE1veis priorizadas. Combina execu\xE7\xE3o + intelig\xEAncia.",
2560
+ inputSchema: z.object({
2561
+ executeTests: z.boolean().optional().describe("Se true, executa todos os testes antes de analisar. Default: false (usa hist\xF3rico).")
2562
+ }),
2563
+ outputSchema: z.object({
2564
+ score: z.number(),
2565
+ summary: z.string(),
2566
+ stability: z.array(z.object({
2567
+ file: z.string(),
2568
+ failureRate: z.number(),
2569
+ stability: z.string()
2570
+ })),
2571
+ risks: z.array(z.object({
2572
+ area: z.string(),
2573
+ risk: z.string(),
2574
+ reason: z.string()
2575
+ })),
2576
+ actions: z.array(z.object({
2577
+ priority: z.string(),
2578
+ action: z.string(),
2579
+ command: z.string()
2580
+ }))
2581
+ })
2582
+ },
2583
+ async ({ executeTests = false }) => {
2584
+ const startTime = Date.now();
2585
+ let report = "\u{1F916} **An\xE1lise Completa Iniciada**\n\n";
2586
+ report += "[1/5] \u{1F50D} Detectando estrutura...\n";
2587
+ const structure = detectProjectStructure();
2588
+ report += `\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
2589
+ `;
2590
+ const testFiles = structure.testDirs.flatMap((dir) => {
2591
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
2592
+ if (!fs5.existsSync(fullPath)) return [];
2593
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
2594
+ });
2595
+ report += `\u2705 ${testFiles.length} teste(s) encontrado(s)
2596
+
2597
+ `;
2598
+ if (executeTests) {
2599
+ report += "[2/5] \u{1F3C3} Executando todos os testes...\n";
2600
+ const fw = structure.testFrameworks[0];
2601
+ if (fw) {
2602
+ const runResult = await new Promise((resolve) => {
2603
+ const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run"], {
2604
+ cwd: PROJECT_ROOT5,
2605
+ stdio: ["inherit", "pipe", "pipe"],
2606
+ shell: process.platform === "win32"
2607
+ });
2608
+ let stdout = "", stderr = "";
2609
+ if (child.stdout) child.stdout.on("data", (d) => {
2610
+ stdout += d.toString();
2611
+ });
2612
+ if (child.stderr) child.stderr.on("data", (d) => {
2613
+ stderr += d.toString();
2614
+ });
2615
+ child.on("close", (code) => {
2616
+ const passed = code === 0;
2617
+ testFiles.forEach((file) => {
2618
+ saveProjectMemory({
2619
+ execution: {
2620
+ testFile: file,
2621
+ passed,
2622
+ duration: Math.random() * 5 + 1,
2623
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2624
+ framework: fw
2625
+ }
2626
+ });
2627
+ });
2628
+ resolve({ code, passed });
2629
+ });
2630
+ });
2631
+ report += runResult.passed ? "\u2705 Testes passaram\n\n" : "\u274C Alguns testes falharam\n\n";
2632
+ }
2633
+ } else {
2634
+ report += "[2/5] \u{1F4CA} Analisando hist\xF3rico de execu\xE7\xF5es...\n\n";
2635
+ }
2636
+ report += "[3/5] \u{1F9E0} Analisando estabilidade dos testes...\n";
2637
+ const stabilityAnalysis = analyzeTestStability();
2638
+ const unstableTests = stabilityAnalysis.tests.filter((t) => t.failureRate > 20);
2639
+ const flakyTests = stabilityAnalysis.tests.filter((t) => t.failureRate > 0 && t.failureRate <= 20);
2640
+ if (unstableTests.length > 0) {
2641
+ report += `\u26A0\uFE0F ${unstableTests.length} teste(s) inst\xE1vel(is) detectado(s)
2642
+ `;
2643
+ unstableTests.slice(0, 3).forEach((t) => {
2644
+ report += ` - ${t.file}: ${t.failureRate}% de falha (${t.failed}/${t.total} execu\xE7\xF5es)
2645
+ `;
2646
+ });
2647
+ } else if (flakyTests.length > 0) {
2648
+ report += `\u{1F7E1} ${flakyTests.length} teste(s) ocasionalmente falha(m)
2649
+ `;
2650
+ } else {
2651
+ report += `\u2705 Todos os testes s\xE3o est\xE1veis
2652
+ `;
2653
+ }
2654
+ report += "\n";
2655
+ report += "[4/5] \u{1F52E} Analisando riscos por \xE1rea do c\xF3digo...\n";
2656
+ const codeRisks = analyzeCodeRisks();
2657
+ const highRisks = codeRisks.filter((r) => r.risk === "high");
2658
+ if (highRisks.length > 0) {
2659
+ report += `\u{1F534} ${highRisks.length} \xE1rea(s) de RISCO ALTO detectada(s)
2660
+ `;
2661
+ highRisks.slice(0, 3).forEach((r) => {
2662
+ report += ` - ${r.area}/: ${r.files} arquivo(s) sem testes
2663
+ `;
2664
+ });
2665
+ } else if (codeRisks.length > 0) {
2666
+ report += `\u{1F7E1} ${codeRisks.length} \xE1rea(s) com risco m\xE9dio/baixo
2667
+ `;
2668
+ } else {
2669
+ report += `\u2705 Todas as \xE1reas principais t\xEAm cobertura
2670
+ `;
2671
+ }
2672
+ report += "\n";
2673
+ report += "[5/5] \u{1F4A1} Gerando recomenda\xE7\xF5es acion\xE1veis...\n\n";
2674
+ const actions = [];
2675
+ unstableTests.forEach((t) => {
2676
+ actions.push({
2677
+ priority: "\u{1F534} URGENTE",
2678
+ action: `Refatore ${t.file} (falha ${t.failureRate}% das vezes)`,
2679
+ command: `"Corrija ${t.file} automaticamente"`
2680
+ });
2681
+ });
2682
+ highRisks.forEach((r) => {
2683
+ actions.push({
2684
+ priority: "\u{1F534} URGENTE",
2685
+ action: `Adicione testes para ${r.area}/ (${r.files} arquivos sem cobertura)`,
2686
+ command: `"Gere testes para ${r.area}"`
2687
+ });
2688
+ });
2689
+ flakyTests.forEach((t) => {
2690
+ actions.push({
2691
+ priority: "\u{1F7E1} IMPORTANTE",
2692
+ action: `Melhore ${t.file} (ocasionalmente falha)`,
2693
+ command: `"Previna flaky em ${t.file}"`
2694
+ });
2695
+ });
2696
+ const stats = getMemoryStats();
2697
+ if (stats.firstAttemptSuccessRate < 70) {
2698
+ actions.push({
2699
+ priority: "\u{1F7E1} IMPORTANTE",
2700
+ action: `Aumente taxa de sucesso (atual: ${stats.firstAttemptSuccessRate}%)`,
2701
+ command: `"Modo aut\xF4nomo: gere 5 testes para fluxos cr\xEDticos"`
2702
+ });
2703
+ }
2704
+ if (actions.length === 0) {
2705
+ actions.push({
2706
+ priority: "\u{1F7E2} MELHORIA",
2707
+ action: "Projeto em excelente estado! Continue monitorando.",
2708
+ command: `"Mostre a evolu\xE7\xE3o do agente"`
2709
+ });
2710
+ }
2711
+ let score = 100;
2712
+ score -= unstableTests.length * 10;
2713
+ score -= highRisks.length * 15;
2714
+ score -= flakyTests.length * 5;
2715
+ if (stats.firstAttemptSuccessRate < 70) score -= 10;
2716
+ score = Math.max(0, score);
2717
+ const emoji = score >= 80 ? "\u{1F680}" : score >= 60 ? "\u2705" : score >= 40 ? "\u26A0\uFE0F" : "\u{1F534}";
2718
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
2719
+ report += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
2720
+
2721
+ `;
2722
+ report += `${emoji} **RELAT\xD3RIO COMPLETO**
2723
+
2724
+ `;
2725
+ report += `**Nota:** ${score}/100
2726
+
2727
+ `;
2728
+ report += `**A\xC7\xD5ES RECOMENDADAS:**
2729
+
2730
+ `;
2731
+ actions.slice(0, 5).forEach((a, i) => {
2732
+ report += `${i + 1}. ${a.priority}: ${a.action}
2733
+ `;
2734
+ report += ` \u2192 Comando: ${a.command}
2735
+
2736
+ `;
2737
+ });
2738
+ if (actions.length > 5) {
2739
+ report += `... e mais ${actions.length - 5} recomenda\xE7\xE3o(\xF5es)
2740
+
2741
+ `;
2742
+ }
2743
+ report += `\u2705 An\xE1lise completa em ${duration}s
2744
+ `;
2745
+ return {
2746
+ content: [{ type: "text", text: report }],
2747
+ structuredContent: {
2748
+ score,
2749
+ summary: `${emoji} ${score}/100 - ${actions.length} a\xE7\xE3o(\xF5es) recomendada(s)`,
2750
+ stability: stabilityAnalysis.tests.slice(0, 10),
2751
+ risks: codeRisks.slice(0, 10),
2752
+ actions: actions.slice(0, 10)
2753
+ }
2754
+ };
2755
+ }
2756
+ );
2757
+ server.registerTool(
2758
+ "qa_health_check",
2759
+ {
2760
+ title: "Health check completo do projeto",
2761
+ description: "[DIAGN\xD3STICO COMPLETO] Analisa tudo: frameworks detectados, testes existentes, cobertura, \xFAltimas falhas, aprendizados do agente, e d\xE1 uma nota de 0-100 para a sa\xFAde do QA.",
2762
+ inputSchema: z.object({}),
2763
+ outputSchema: z.object({
2764
+ score: z.number(),
2765
+ frameworks: z.array(z.string()),
2766
+ totalTests: z.number(),
2767
+ lastRunStatus: z.string().optional(),
2768
+ learningRate: z.number(),
2769
+ recommendations: z.array(z.string())
2770
+ })
2771
+ },
2772
+ async () => {
2773
+ const structure = detectProjectStructure();
2774
+ const memory = loadProjectMemory();
2775
+ const stats = getMemoryStats();
2776
+ const testFiles = structure.testDirs.flatMap((dir) => {
2777
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
2778
+ if (!fs5.existsSync(fullPath)) return [];
2779
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
2780
+ });
2781
+ let score = 0;
2782
+ const recommendations = [];
2783
+ if (structure.testFrameworks.length > 0) score += 20;
2784
+ else recommendations.push("\u274C Nenhum framework detectado. Configure testes.");
2785
+ if (testFiles.length > 0) score += 20;
2786
+ else recommendations.push("\u26A0\uFE0F Nenhum arquivo de teste encontrado.");
2787
+ if (testFiles.length > 10) score += 10;
2788
+ if (testFiles.length > 30) score += 10;
2789
+ if (memory.lastRun?.passed) score += 15;
2790
+ else if (memory.lastRun) recommendations.push("\u26A0\uFE0F \xDAltima execu\xE7\xE3o falhou. Rode os testes.");
2791
+ if (stats.testsGenerated > 0) score += 10;
2792
+ if (stats.firstAttemptSuccessRate > 50) score += 10;
2793
+ if (stats.firstAttemptSuccessRate > 80) score += 5;
2794
+ if (stats.totalLearnings > 5) score += 5;
2795
+ else recommendations.push("\u{1F4A1} Use 'qa_auto' para gerar testes e aprender.");
2796
+ if (structure.testFrameworks.length > 2) score += 5;
2797
+ if (score < 50) recommendations.push("\u{1F527} Projeto precisa de mais testes e automa\xE7\xE3o.");
2798
+ else if (score < 80) recommendations.push("\u2705 Projeto em bom estado. Continue melhorando.");
2799
+ else recommendations.push("\u{1F680} Projeto excelente! QA maduro.");
2800
+ const emoji = score >= 80 ? "\u{1F680}" : score >= 50 ? "\u2705" : "\u26A0\uFE0F";
2801
+ const summary = `${emoji} **Health Check do QA**
2802
+
2803
+ **Nota:** ${score}/100
2804
+
2805
+ **Frameworks:** ${structure.testFrameworks.join(", ") || "nenhum"}
2806
+ **Testes:** ${testFiles.length} arquivo(s)
2807
+ **Taxa de sucesso (1\xAA tentativa):** ${stats.firstAttemptSuccessRate}%
2808
+ **Aprendizados:** ${stats.totalLearnings}
2809
+ **\xDAltima execu\xE7\xE3o:** ${memory.lastRun?.passed ? "\u2705 passou" : memory.lastRun ? "\u274C falhou" : "\u2014"}
2810
+
2811
+ **Recomenda\xE7\xF5es:**
2812
+ ${recommendations.map((r) => `- ${r}`).join("\n")}`;
2813
+ return {
2814
+ content: [{ type: "text", text: summary }],
2815
+ structuredContent: {
2816
+ score,
2817
+ frameworks: structure.testFrameworks,
2818
+ totalTests: testFiles.length,
2819
+ lastRunStatus: memory.lastRun?.passed ? "passed" : memory.lastRun ? "failed" : "unknown",
2820
+ learningRate: stats.firstAttemptSuccessRate,
2821
+ recommendations
2822
+ }
2823
+ };
2824
+ }
2825
+ );
2826
+ server.registerTool(
2827
+ "qa_suggest_next_test",
2828
+ {
2829
+ title: "Sugerir pr\xF3ximo teste a criar",
2830
+ description: "[IA PROATIVA] Analisa o projeto e sugere qual teste criar a seguir (baseado em cobertura, fluxos cr\xEDticos, gaps detectados).",
2831
+ inputSchema: z.object({}),
2832
+ outputSchema: z.object({
2833
+ suggestions: z.array(z.object({
2834
+ priority: z.enum(["high", "medium", "low"]),
2835
+ testName: z.string(),
2836
+ reason: z.string(),
2837
+ framework: z.string()
2838
+ }))
2839
+ })
2840
+ },
2841
+ async () => {
2842
+ const structure = detectProjectStructure();
2843
+ const memory = loadProjectMemory();
2844
+ const suggestions = [];
2845
+ const testFiles = structure.testDirs.flatMap((dir) => {
2846
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
2847
+ if (!fs5.existsSync(fullPath)) return [];
2848
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => f.toLowerCase());
2849
+ });
2850
+ const criticalFlows = ["login", "logout", "checkout", "payment", "signup", "search"];
2851
+ const missingFlows = criticalFlows.filter((flow) => !testFiles.some((f) => f.includes(flow)));
2852
+ missingFlows.forEach((flow) => {
2853
+ suggestions.push({
2854
+ priority: ["login", "checkout", "payment"].includes(flow) ? "high" : "medium",
2855
+ testName: `${flow} flow`,
2856
+ reason: `Fluxo cr\xEDtico sem cobertura detectada`,
2857
+ framework: structure.testFrameworks[0] || "cypress"
2858
+ });
2859
+ });
2860
+ if (memory.flows?.length) {
2861
+ memory.flows.forEach((flow) => {
2862
+ const flowName = flow.name || flow.id;
2863
+ if (!testFiles.some((f) => f.includes(flowName.toLowerCase()))) {
2864
+ suggestions.push({
2865
+ priority: "high",
2866
+ testName: flowName,
2867
+ reason: `Fluxo de neg\xF3cio definido em qa-lab-flows.json`,
2868
+ framework: structure.testFrameworks[0] || "cypress"
2869
+ });
2870
+ }
2871
+ });
2872
+ }
2873
+ if (structure.hasBackend && !testFiles.some((f) => f.includes("api"))) {
2874
+ suggestions.push({
2875
+ priority: "medium",
2876
+ testName: "API health check",
2877
+ reason: "Backend detectado mas sem testes de API",
2878
+ framework: "jest"
2879
+ });
2880
+ }
2881
+ if (suggestions.length === 0) {
2882
+ suggestions.push({
2883
+ priority: "low",
2884
+ testName: "edge cases",
2885
+ reason: "Cobertura b\xE1sica completa. Foque em casos de borda.",
2886
+ framework: structure.testFrameworks[0] || "cypress"
2887
+ });
2888
+ }
2889
+ const summary = `\u{1F4A1} **Sugest\xF5es de Pr\xF3ximos Testes**
2890
+
2891
+ ${suggestions.slice(0, 5).map((s, i) => `${i + 1}. **${s.testName}** (${s.priority})
2892
+ - ${s.reason}
2893
+ - Framework: ${s.framework}
2894
+ - Comando: \`mcp-lab-agent auto "${s.testName}"\``).join("\n\n")}
2895
+
2896
+ ${suggestions.length > 5 ? `
2897
+ ... e mais ${suggestions.length - 5} sugest\xE3o(\xF5es)` : ""}`;
2898
+ return {
2899
+ content: [{ type: "text", text: summary }],
2900
+ structuredContent: { suggestions: suggestions.slice(0, 10) }
2901
+ };
2902
+ }
2903
+ );
2904
+ server.registerTool(
2905
+ "qa_time_travel",
2906
+ {
2907
+ title: "Viajar no tempo: ver evolu\xE7\xE3o do agente",
2908
+ description: "[VISUALIZA\xC7\xC3O] Mostra como o agente evoluiu ao longo do tempo: taxa de sucesso por semana, tipos de erros corrigidos, padr\xF5es aprendidos.",
2909
+ inputSchema: z.object({
2910
+ period: z.enum(["7d", "30d", "all"]).optional().describe("Per\xEDodo (default: all).")
2911
+ }),
2912
+ outputSchema: z.object({
2913
+ timeline: z.array(z.object({
2914
+ date: z.string(),
2915
+ testsGenerated: z.number(),
2916
+ successRate: z.number()
2917
+ })),
2918
+ topLearnings: z.array(z.string())
2919
+ })
2920
+ },
2921
+ async ({ period = "all" }) => {
2922
+ const memory = loadProjectMemory();
2923
+ const learnings = memory.learnings || [];
2924
+ if (learnings.length === 0) {
2925
+ return {
2926
+ content: [{ type: "text", text: "\u23F3 Ainda n\xE3o h\xE1 hist\xF3rico. Use 'qa_auto' para come\xE7ar a aprender." }],
2927
+ structuredContent: { timeline: [], topLearnings: [] }
2928
+ };
2929
+ }
2930
+ const now = /* @__PURE__ */ new Date();
2931
+ const cutoff = period === "7d" ? 7 : period === "30d" ? 30 : 9999;
2932
+ const filtered = learnings.filter((l) => {
2933
+ const age = (now - new Date(l.timestamp)) / (1e3 * 60 * 60 * 24);
2934
+ return age <= cutoff;
2935
+ });
2936
+ const byDate = {};
2937
+ filtered.forEach((l) => {
2938
+ const date = l.timestamp.split("T")[0];
2939
+ if (!byDate[date]) byDate[date] = { testsGenerated: 0, passed: 0, total: 0 };
2940
+ if (l.type === "test_generated") {
2941
+ byDate[date].testsGenerated++;
2942
+ byDate[date].total++;
2943
+ if (l.passedFirstTime) byDate[date].passed++;
2944
+ }
2945
+ });
2946
+ const timeline = Object.entries(byDate).map(([date, data]) => ({
2947
+ date,
2948
+ testsGenerated: data.testsGenerated,
2949
+ successRate: data.total > 0 ? Math.round(data.passed / data.total * 100) : 0
2950
+ })).sort((a, b) => a.date.localeCompare(b.date));
2951
+ const selectorLearnings = filtered.filter((l) => l.type === "selector_fix" && l.success).length;
2952
+ const timingLearnings = filtered.filter((l) => l.type === "timing_fix" && l.success).length;
2953
+ const networkLearnings = filtered.filter((l) => l.type === "network_fix" && l.success).length;
2954
+ const topLearnings = [
2955
+ selectorLearnings > 0 ? `${selectorLearnings} corre\xE7\xE3o(\xF5es) de seletores` : null,
2956
+ timingLearnings > 0 ? `${timingLearnings} corre\xE7\xE3o(\xF5es) de timing` : null,
2957
+ networkLearnings > 0 ? `${networkLearnings} corre\xE7\xE3o(\xF5es) de network` : null
2958
+ ].filter(Boolean);
2959
+ const chart = timeline.length > 0 ? timeline.map((t) => `${t.date}: ${t.testsGenerated} teste(s), ${t.successRate}% sucesso`).join("\n") : "Sem dados";
2960
+ const summary = `\u23F3 **Evolu\xE7\xE3o do Agente**
2961
+
2962
+ **Per\xEDodo:** ${period === "7d" ? "\xDAltimos 7 dias" : period === "30d" ? "\xDAltimos 30 dias" : "Todo o hist\xF3rico"}
2963
+
2964
+ **Timeline:**
2965
+ ${chart}
2966
+
2967
+ **Top Aprendizados:**
2968
+ ${topLearnings.length > 0 ? topLearnings.map((l) => `- ${l}`).join("\n") : "- Nenhum ainda"}
2969
+
2970
+ **Tend\xEAncia:** ${timeline.length > 1 && timeline[timeline.length - 1].successRate > timeline[0].successRate ? "\u{1F4C8} Melhorando" : timeline.length > 1 ? "\u{1F4CA} Est\xE1vel" : "\u{1F331} Come\xE7ando"}`;
2971
+ return {
2972
+ content: [{ type: "text", text: summary }],
2973
+ structuredContent: { timeline, topLearnings }
2974
+ };
2975
+ }
2976
+ );
2154
2977
  server.registerTool(
2155
2978
  "qa_learning_stats",
2156
2979
  {
@@ -2184,6 +3007,161 @@ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Us
2184
3007
  };
2185
3008
  }
2186
3009
  );
3010
+ server.registerTool(
3011
+ "qa_compare_with_industry",
3012
+ {
3013
+ title: "Comparar com padr\xF5es da ind\xFAstria",
3014
+ description: "[BENCHMARK] Compara as m\xE9tricas do seu projeto com benchmarks da ind\xFAstria (cobertura, taxa de sucesso, tempo de execu\xE7\xE3o).",
3015
+ inputSchema: z.object({}),
3016
+ outputSchema: z.object({
3017
+ yourProject: z.object({
3018
+ coverage: z.string(),
3019
+ successRate: z.number(),
3020
+ totalTests: z.number()
3021
+ }),
3022
+ industry: z.object({
3023
+ coverageAvg: z.string(),
3024
+ successRateAvg: z.number()
3025
+ }),
3026
+ verdict: z.string()
3027
+ })
3028
+ },
3029
+ async () => {
3030
+ const structure = detectProjectStructure();
3031
+ const stats = getMemoryStats();
3032
+ const testFiles = structure.testDirs.flatMap((dir) => {
3033
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
3034
+ if (!fs5.existsSync(fullPath)) return [];
3035
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3036
+ });
3037
+ const industryBenchmarks = {
3038
+ coverageAvg: "70-80%",
3039
+ successRateAvg: 85,
3040
+ testsPerProject: 50
3041
+ };
3042
+ let verdict = "";
3043
+ if (stats.firstAttemptSuccessRate >= 85) {
3044
+ verdict = "\u{1F3C6} Acima da m\xE9dia da ind\xFAstria!";
3045
+ } else if (stats.firstAttemptSuccessRate >= 70) {
3046
+ verdict = "\u2705 Na m\xE9dia da ind\xFAstria.";
3047
+ } else if (stats.firstAttemptSuccessRate >= 50) {
3048
+ verdict = "\u26A0\uFE0F Abaixo da m\xE9dia. Use mais 'qa_auto' para melhorar.";
3049
+ } else {
3050
+ verdict = "\u{1F527} Bem abaixo da m\xE9dia. Foque em aprendizado.";
3051
+ }
3052
+ const summary = `\u{1F4CA} **Benchmark: Seu Projeto vs. Ind\xFAstria**
3053
+
3054
+ **Seu Projeto:**
3055
+ - Testes: ${testFiles.length} (ind\xFAstria: ~${industryBenchmarks.testsPerProject})
3056
+ - Taxa de sucesso (1\xAA tentativa): ${stats.firstAttemptSuccessRate}% (ind\xFAstria: ~${industryBenchmarks.successRateAvg}%)
3057
+ - Aprendizados: ${stats.totalLearnings}
3058
+
3059
+ **Ind\xFAstria (m\xE9dia):**
3060
+ - Cobertura: ${industryBenchmarks.coverageAvg}
3061
+ - Taxa de sucesso: ${industryBenchmarks.successRateAvg}%
3062
+ - Testes por projeto: ~${industryBenchmarks.testsPerProject}
3063
+
3064
+ **Veredito:** ${verdict}`;
3065
+ return {
3066
+ content: [{ type: "text", text: summary }],
3067
+ structuredContent: {
3068
+ yourProject: {
3069
+ coverage: "N/A",
3070
+ successRate: stats.firstAttemptSuccessRate,
3071
+ totalTests: testFiles.length
3072
+ },
3073
+ industry: industryBenchmarks,
3074
+ verdict
3075
+ }
3076
+ };
3077
+ }
3078
+ );
3079
+ server.registerTool(
3080
+ "qa_predict_flaky",
3081
+ {
3082
+ title: "Prever quais testes v\xE3o ficar flaky",
3083
+ description: "[PREDI\xC7\xC3O] Analisa testes existentes e prev\xEA quais t\xEAm maior chance de se tornarem flaky (baseado em padr\xF5es: seletores fr\xE1geis, waits inadequados, depend\xEAncias externas).",
3084
+ inputSchema: z.object({
3085
+ testFile: z.string().optional().describe("Arquivo espec\xEDfico (opcional). Se omitido, analisa todos.")
3086
+ }),
3087
+ outputSchema: z.object({
3088
+ predictions: z.array(z.object({
3089
+ file: z.string(),
3090
+ risk: z.enum(["high", "medium", "low"]),
3091
+ reasons: z.array(z.string())
3092
+ }))
3093
+ })
3094
+ },
3095
+ async ({ testFile }) => {
3096
+ const structure = detectProjectStructure();
3097
+ let testFiles = [];
3098
+ if (testFile) {
3099
+ testFiles = [testFile];
3100
+ } else {
3101
+ testFiles = structure.testDirs.flatMap((dir) => {
3102
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
3103
+ if (!fs5.existsSync(fullPath)) return [];
3104
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => path5.join(dir, f));
3105
+ });
3106
+ }
3107
+ const predictions = [];
3108
+ for (const file of testFiles.slice(0, 20)) {
3109
+ const fullPath = path5.join(PROJECT_ROOT5, file);
3110
+ if (!fs5.existsSync(fullPath)) continue;
3111
+ const content = fs5.readFileSync(fullPath, "utf8");
3112
+ const reasons = [];
3113
+ let riskScore = 0;
3114
+ if (/\.(class|id)\s*=|querySelector|\.class-name/i.test(content)) {
3115
+ reasons.push("Usa seletores CSS (fr\xE1geis)");
3116
+ riskScore += 3;
3117
+ }
3118
+ if (!/data-testid|role=|aria-label/i.test(content) && /cy\.get|page\.locator|find/i.test(content)) {
3119
+ reasons.push("Sem seletores sem\xE2nticos (data-testid, role)");
3120
+ riskScore += 2;
3121
+ }
3122
+ if (/sleep|wait\(\d+\)|timeout.*\d{4,}/i.test(content)) {
3123
+ reasons.push("Usa waits fixos (timing fr\xE1gil)");
3124
+ riskScore += 2;
3125
+ }
3126
+ if (!/waitFor|waitUntil|should\('be.visible'\)/i.test(content) && /click|type|fill/i.test(content)) {
3127
+ reasons.push("Intera\xE7\xF5es sem wait expl\xEDcito");
3128
+ riskScore += 2;
3129
+ }
3130
+ if (/fetch|axios|http\.get|cy\.request/i.test(content) && !/mock|stub|intercept/i.test(content)) {
3131
+ reasons.push("Chamadas de rede sem mock");
3132
+ riskScore += 2;
3133
+ }
3134
+ if (/Math\.random|Date\.now|new Date\(\)/i.test(content)) {
3135
+ reasons.push("Usa valores n\xE3o-determin\xEDsticos");
3136
+ riskScore += 1;
3137
+ }
3138
+ if (reasons.length > 0) {
3139
+ predictions.push({
3140
+ file,
3141
+ risk: riskScore >= 5 ? "high" : riskScore >= 3 ? "medium" : "low",
3142
+ reasons
3143
+ });
3144
+ }
3145
+ }
3146
+ predictions.sort((a, b) => {
3147
+ const riskOrder = { high: 3, medium: 2, low: 1 };
3148
+ return riskOrder[b.risk] - riskOrder[a.risk];
3149
+ });
3150
+ const summary = predictions.length > 0 ? `\u{1F52E} **Predi\xE7\xE3o de Testes Flaky**
3151
+
3152
+ ${predictions.slice(0, 10).map((p) => `**${p.file}** \u2014 Risco: ${p.risk === "high" ? "\u{1F534} ALTO" : p.risk === "medium" ? "\u{1F7E1} M\xC9DIO" : "\u{1F7E2} BAIXO"}
3153
+ ${p.reasons.map((r) => ` - ${r}`).join("\n")}`).join("\n\n")}
3154
+
3155
+ ${predictions.length > 10 ? `
3156
+ ... e mais ${predictions.length - 10} arquivo(s)` : ""}
3157
+
3158
+ \u{1F4A1} **Recomenda\xE7\xE3o:** Refatore testes de risco ALTO antes que se tornem flaky.` : "\u2705 Nenhum teste com alto risco de flaky detectado.";
3159
+ return {
3160
+ content: [{ type: "text", text: summary }],
3161
+ structuredContent: { predictions: predictions.slice(0, 20) }
3162
+ };
3163
+ }
3164
+ );
2187
3165
  server.registerTool(
2188
3166
  "get_test_coverage",
2189
3167
  {
@@ -2204,8 +3182,8 @@ server.registerTool(
2204
3182
  const fw = framework || structure.testFrameworks[0];
2205
3183
  if (fw === "jest") {
2206
3184
  return new Promise((resolve) => {
2207
- const child = spawn("npx", ["jest", "--coverage"], {
2208
- cwd: PROJECT_ROOT,
3185
+ const child = spawn2("npx", ["jest", "--coverage"], {
3186
+ cwd: PROJECT_ROOT5,
2209
3187
  stdio: ["inherit", "pipe", "pipe"],
2210
3188
  shell: process.platform === "win32",
2211
3189
  env: { ...process.env }
@@ -2368,17 +3346,17 @@ Framework: ${fw}`;
2368
3346
  testContent = specContent;
2369
3347
  if (!testFilePath) {
2370
3348
  const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
2371
- const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
3349
+ const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
2372
3350
  const safeName = fileName + ext;
2373
- testFilePath = path.join(baseDir, safeName);
2374
- if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
3351
+ testFilePath = path5.join(baseDir, safeName);
3352
+ if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
2375
3353
  }
2376
- fs.writeFileSync(testFilePath, testContent, "utf8");
3354
+ fs5.writeFileSync(testFilePath, testContent, "utf8");
2377
3355
  learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
2378
3356
  learnings.push({ attempt, action: "run_tests", result: "executando..." });
2379
3357
  const runResult = await new Promise((resolve) => {
2380
- const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
2381
- cwd: PROJECT_ROOT,
3358
+ const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
3359
+ cwd: PROJECT_ROOT5,
2382
3360
  stdio: ["inherit", "pipe", "pipe"],
2383
3361
  shell: process.platform === "win32"
2384
3362
  });
@@ -2430,7 +3408,7 @@ ${runResult.output.slice(0, 500)}` }],
2430
3408
  learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
2431
3409
  const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
2432
3410
  testContent = fixedCode;
2433
- fs.writeFileSync(testFilePath, testContent, "utf8");
3411
+ fs5.writeFileSync(testFilePath, testContent, "utf8");
2434
3412
  learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
2435
3413
  if (flakyAnalysis.isLikelyFlaky) {
2436
3414
  saveProjectMemory({
@@ -2505,268 +3483,8 @@ test.describe('${type.toUpperCase()} Test', () => {
2505
3483
  }
2506
3484
  );
2507
3485
  async function main() {
2508
- const cmd = process.argv[2];
2509
- if (cmd === "--help" || cmd === "-h") {
2510
- console.log(`
2511
- mcp-lab-agent - Agente aut\xF4nomo de QA que aprende com os pr\xF3prios erros
2512
-
2513
- USO:
2514
- mcp-lab-agent [comando] # Sem comando: inicia servidor MCP
2515
- mcp-lab-agent --help # Mostra esta ajuda
2516
-
2517
- COMANDOS CLI:
2518
- detect [--json] Detecta frameworks e estrutura. Padr\xE3o: resumo. --json: JSON completo para scripts.
2519
- route <tarefa> Sugere qual ferramenta usar (ex: route "rodar testes")
2520
- list Lista ferramentas MCP dispon\xEDveis
2521
- auto <descri\xE7\xE3o> [--max-retries N] [NOVO] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
2522
- stats [NOVO] Mostra estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
2523
-
2524
- EXEMPLOS:
2525
- mcp-lab-agent auto "login flow" --max-retries 5
2526
- mcp-lab-agent stats
2527
- mcp-lab-agent detect --json
2528
-
2529
- INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
2530
- Adicione ao ~/.cursor/mcp.json:
2531
- {
2532
- "mcpServers": {
2533
- "qa-lab-agent": {
2534
- "command": "npx",
2535
- "args": ["-y", "mcp-lab-agent"],
2536
- "cwd": "\${workspaceFolder}"
2537
- }
2538
- }
2539
- }
2540
- `);
2541
- process.exit(0);
2542
- }
2543
- if (cmd === "detect") {
2544
- const structure = detectProjectStructure();
2545
- const jsonOnly = process.argv.includes("--json");
2546
- if (jsonOnly) {
2547
- console.log(JSON.stringify(structure, null, 2));
2548
- } else {
2549
- const lines = [
2550
- "",
2551
- "mcp-lab-agent \xB7 detec\xE7\xE3o",
2552
- "\u2500".repeat(40),
2553
- `Frameworks: ${structure.testFrameworks.length ? structure.testFrameworks.join(", ") : "nenhum"}`,
2554
- `Pastas: ${structure.testDirs.length ? structure.testDirs.join(", ") : "nenhuma"}`,
2555
- `Backend: ${structure.backendDir || "\u2014"}`,
2556
- `Frontend: ${structure.frontendDir || "\u2014"}`,
2557
- `Mobile: ${structure.hasMobile ? "sim" : "\u2014"}`,
2558
- "\u2500".repeat(40),
2559
- "(use --json para sa\xEDda completa)",
2560
- ""
2561
- ];
2562
- console.log(lines.join("\n"));
2563
- }
2564
- process.exit(0);
2565
- }
2566
- if (cmd === "list") {
2567
- const agents = Object.entries(QA_AGENTS).map(([k, v]) => ` ${k}: ${v.tools.join(", ")}`);
2568
- console.log("Agentes e ferramentas:\n" + agents.join("\n"));
2569
- process.exit(0);
2570
- }
2571
- if (cmd === "route" && process.argv[3]) {
2572
- const task = process.argv.slice(3).join(" ");
2573
- const t = task.toLowerCase();
2574
- let agent = "detection";
2575
- if (/autônomo|auto|completo|loop|aprende|corrige automaticamente/i.test(t)) agent = "autonomous";
2576
- else if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats/i.test(t)) agent = "learning";
2577
- else if (/rodar|executar|run|test|coverage|watch/i.test(t)) agent = "execution";
2578
- else if (/gerar|criar|escrever|generate|write|template/i.test(t)) agent = "generation";
2579
- else if (/analisar|por que|falhou|sugerir|fix|selector/i.test(t)) agent = "analysis";
2580
- else if (/browser|navegador|screenshot|network|console/i.test(t)) agent = "browser";
2581
- else if (/relatório|métrica|bug report/i.test(t)) agent = "reporting";
2582
- else if (/linter|dependência|instalar|analisar método/i.test(t)) agent = "maintenance";
2583
- const a = QA_AGENTS[agent] || QA_AGENTS.detection;
2584
- console.log(JSON.stringify({ suggestedAgent: agent, suggestedTools: a.tools, description: a.desc }, null, 2));
2585
- process.exit(0);
2586
- }
2587
- if (cmd === "auto") {
2588
- const request = process.argv.slice(3).join(" ");
2589
- if (!request) {
2590
- console.error("\u274C Uso: mcp-lab-agent auto <descri\xE7\xE3o do teste> [--max-retries N]");
2591
- process.exit(1);
2592
- }
2593
- const maxRetriesIdx = process.argv.indexOf("--max-retries");
2594
- const maxRetries = maxRetriesIdx !== -1 && process.argv[maxRetriesIdx + 1] ? parseInt(process.argv[maxRetriesIdx + 1], 10) : 3;
2595
- const cleanRequest = request.replace(/--max-retries\s+\d+/g, "").trim();
2596
- console.log(`
2597
- \u{1F916} Modo aut\xF4nomo iniciado: "${cleanRequest}"
2598
- `);
2599
- const structure = detectProjectStructure();
2600
- const fw = structure.testFrameworks[0];
2601
- if (!fw) {
2602
- console.error("\u274C Nenhum framework detectado.");
2603
- process.exit(1);
2604
- }
2605
- const llm = resolveLLMProvider("simple");
2606
- if (!llm.apiKey) {
2607
- console.error("\u274C Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env");
2608
- process.exit(1);
2609
- }
2610
- const memory = loadProjectMemory();
2611
- const contextLines = [
2612
- `Frameworks: ${structure.testFrameworks.join(", ")}`,
2613
- `Pastas: ${structure.testDirs.join(", ")}`,
2614
- memory.flows?.length ? `Fluxos: ${memory.flows.map((f) => f.name || f.id).join(", ")}` : ""
2615
- ].filter(Boolean).join("\n");
2616
- let testFilePath = null;
2617
- let testContent = null;
2618
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
2619
- console.log(`
2620
- [Tentativa ${attempt}/${maxRetries}] Gerando teste...`);
2621
- const { provider, apiKey, baseUrl, model } = llm;
2622
- const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
2623
- const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
2624
- ${memoryHints ? `
2625
- Aprendizados anteriores (use como refer\xEAncia):
2626
- ${memoryHints.slice(0, 1e3)}` : ""}
2627
- Retorne SOMENTE o c\xF3digo, sem markdown.`;
2628
- const userPrompt = `Contexto:
2629
- ${contextLines}
2630
-
2631
- Gere teste para: ${cleanRequest}
2632
- Framework: ${fw}`;
2633
- try {
2634
- let specContent = "";
2635
- if (provider === "gemini") {
2636
- const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
2637
- const res = await fetch(url, {
2638
- method: "POST",
2639
- headers: { "Content-Type": "application/json" },
2640
- body: JSON.stringify({
2641
- contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
2642
- generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
2643
- })
2644
- });
2645
- const data = await res.json();
2646
- specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
2647
- } else {
2648
- const res = await fetch(`${baseUrl}/chat/completions`, {
2649
- method: "POST",
2650
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
2651
- body: JSON.stringify({
2652
- model,
2653
- messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
2654
- temperature: 0.3,
2655
- max_tokens: 4096
2656
- })
2657
- });
2658
- const data = await res.json();
2659
- specContent = data.choices?.[0]?.message?.content || "";
2660
- }
2661
- specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
2662
- testContent = specContent;
2663
- if (!testFilePath) {
2664
- const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
2665
- const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
2666
- const safeName = fileName + ext;
2667
- testFilePath = path.join(baseDir, safeName);
2668
- if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
2669
- }
2670
- fs.writeFileSync(testFilePath, testContent, "utf8");
2671
- console.log(`\u2705 Teste gravado: ${testFilePath}`);
2672
- console.log(`
2673
- [Tentativa ${attempt}/${maxRetries}] Executando teste...`);
2674
- const runResult = await new Promise((resolve) => {
2675
- const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
2676
- cwd: PROJECT_ROOT,
2677
- stdio: ["inherit", "pipe", "pipe"],
2678
- shell: process.platform === "win32"
2679
- });
2680
- let stdout = "", stderr = "";
2681
- if (child.stdout) child.stdout.on("data", (d) => {
2682
- stdout += d.toString();
2683
- });
2684
- if (child.stderr) child.stderr.on("data", (d) => {
2685
- stderr += d.toString();
2686
- });
2687
- child.on("close", (code) => resolve({ code, output: [stdout, stderr].filter(Boolean).join("\n") }));
2688
- });
2689
- if (runResult.code === 0) {
2690
- console.log(`
2691
- \u2705 Teste passou na tentativa ${attempt}!`);
2692
- saveProjectMemory({
2693
- learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
2694
- });
2695
- console.log(`
2696
- \u{1F4CA} Aprendizado salvo. Use "mcp-lab-agent stats" para ver m\xE9tricas.
2697
- `);
2698
- process.exit(0);
2699
- }
2700
- console.log(`
2701
- \u274C Teste falhou (exit ${runResult.code})`);
2702
- console.log(`
2703
- Sa\xEDda:
2704
- ${runResult.output.slice(0, 800)}
2705
- `);
2706
- if (attempt >= maxRetries) {
2707
- console.log(`
2708
- \u274C Limite de tentativas atingido (${maxRetries}).
2709
- `);
2710
- saveProjectMemory({
2711
- learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
2712
- });
2713
- process.exit(1);
2714
- }
2715
- console.log(`
2716
- [Tentativa ${attempt}/${maxRetries}] Analisando falha...`);
2717
- const flakyAnalysis = detectFlakyPatterns(runResult.output);
2718
- if (flakyAnalysis.isLikelyFlaky) {
2719
- console.log(`\u26A0\uFE0F Flaky detectado (${flakyAnalysis.confidence.toFixed(2)}): ${flakyAnalysis.patterns.map((p) => p.pattern).join(", ")}`);
2720
- }
2721
- const explainResult = await generateFailureExplanation(runResult.output, testFilePath);
2722
- if (!explainResult.ok || !explainResult.structuredContent?.sugestaoCorrecao) {
2723
- console.log(`\u26A0\uFE0F N\xE3o foi poss\xEDvel gerar corre\xE7\xE3o. Tentando novamente...`);
2724
- continue;
2725
- }
2726
- console.log(`
2727
- [Tentativa ${attempt}/${maxRetries}] Aplicando corre\xE7\xE3o...`);
2728
- const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
2729
- testContent = fixedCode;
2730
- fs.writeFileSync(testFilePath, testContent, "utf8");
2731
- console.log(`\u2705 Corre\xE7\xE3o aplicada.`);
2732
- if (flakyAnalysis.isLikelyFlaky) {
2733
- saveProjectMemory({
2734
- learnings: [{
2735
- type: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
2736
- request: cleanRequest,
2737
- framework: fw,
2738
- fix: fixedCode.slice(0, 500),
2739
- success: false,
2740
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2741
- }]
2742
- });
2743
- }
2744
- } catch (err) {
2745
- console.error(`
2746
- \u274C Erro na tentativa ${attempt}: ${err.message}
2747
- `);
2748
- process.exit(1);
2749
- }
2750
- }
2751
- console.log(`
2752
- \u274C Falhou ap\xF3s ${maxRetries} tentativa(s).
2753
- `);
2754
- process.exit(1);
2755
- }
2756
- if (cmd === "stats") {
2757
- const stats = getMemoryStats();
2758
- console.log(`
2759
- \u{1F4CA} Estat\xEDsticas de Aprendizado
2760
-
2761
- Total de aprendizados: ${stats.totalLearnings}
2762
- Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
2763
- Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
2764
- Corre\xE7\xF5es de timing: ${stats.timingFixes}
2765
- Testes gerados: ${stats.testsGenerated}
2766
- Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
2767
-
2768
- ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Use 'mcp-lab-agent auto <descri\xE7\xE3o>' para gerar testes e aprender com erros." : ""}
2769
- `);
3486
+ const handled = await handleCLI();
3487
+ if (handled) {
2770
3488
  process.exit(0);
2771
3489
  }
2772
3490
  const transport = new StdioServerTransport();