mcp-lab-agent 1.1.2 → 2.0.0

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
@@ -58,11 +58,33 @@ function saveProjectMemory(updates) {
58
58
  if (updates.conventions) data.conventions = { ...data.conventions, ...updates.conventions };
59
59
  if (updates.selectors) data.selectors = [.../* @__PURE__ */ new Set([...data.selectors || [], ...updates.selectors])].slice(-100);
60
60
  if (updates.lastRun) data.lastRun = updates.lastRun;
61
+ if (updates.learnings) {
62
+ data.learnings = data.learnings || [];
63
+ data.learnings.push(...updates.learnings);
64
+ if (data.learnings.length > 200) data.learnings = data.learnings.slice(-150);
65
+ }
61
66
  data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
62
67
  fs.writeFileSync(MEMORY_FILE, JSON.stringify(data, null, 2), "utf8");
63
68
  } catch {
64
69
  }
65
70
  }
71
+ function getMemoryStats() {
72
+ const memory = loadProjectMemory();
73
+ const learnings = memory.learnings || [];
74
+ const successfulFixes = learnings.filter((l) => l.success);
75
+ const selectorFixes = learnings.filter((l) => l.type === "selector_fix");
76
+ const timingFixes = learnings.filter((l) => l.type === "timing_fix");
77
+ const totalTests = learnings.filter((l) => l.type === "test_generated").length;
78
+ const firstAttemptSuccess = learnings.filter((l) => l.type === "test_generated" && l.passedFirstTime).length;
79
+ return {
80
+ totalLearnings: learnings.length,
81
+ successfulFixes: successfulFixes.length,
82
+ selectorFixes: selectorFixes.length,
83
+ timingFixes: timingFixes.length,
84
+ testsGenerated: totalTests,
85
+ firstAttemptSuccessRate: totalTests > 0 ? Math.round(firstAttemptSuccess / totalTests * 100) : 0
86
+ };
87
+ }
66
88
  var FLAKY_PATTERNS = [
67
89
  { 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." },
68
90
  { 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." },
@@ -612,12 +634,14 @@ Requisi\xE7\xF5es: ${networkRequests.length}`;
612
634
  }
613
635
  );
614
636
  var QA_AGENTS = {
637
+ autonomous: { tools: ["qa_auto"], desc: "Modo aut\xF4nomo: gera, roda, corrige e aprende (loop completo)" },
615
638
  detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
616
639
  execution: { tools: ["run_tests", "watch_tests", "get_test_coverage"], desc: "Execu\xE7\xE3o de testes e cobertura" },
617
640
  generation: { tools: ["generate_tests", "write_test", "create_test_template"], desc: "Gera\xE7\xE3o de testes com LLM" },
618
641
  analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
619
642
  browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
620
643
  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" },
621
645
  maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
622
646
  };
623
647
  server.registerTool(
@@ -637,6 +661,12 @@ server.registerTool(
637
661
  },
638
662
  async ({ task }) => {
639
663
  const t = task.toLowerCase();
664
+ 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 } };
666
+ }
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 } };
669
+ }
640
670
  if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
641
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 } };
642
672
  }
@@ -1225,6 +1255,80 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
1225
1255
  const data = await res.json();
1226
1256
  return data.choices?.[0]?.message?.content || "";
1227
1257
  }
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
+ }
1228
1332
  server.registerTool(
1229
1333
  "por_que_falhou",
1230
1334
  {
@@ -2047,6 +2151,39 @@ server.registerTool(
2047
2151
  });
2048
2152
  }
2049
2153
  );
2154
+ server.registerTool(
2155
+ "qa_learning_stats",
2156
+ {
2157
+ title: "Estat\xEDsticas de aprendizado",
2158
+ description: "[M\xC9TRICAS] Retorna m\xE9tricas de aprendizado do agente: quantos testes gerados, taxa de sucesso na primeira tentativa, corre\xE7\xF5es aplicadas, etc.",
2159
+ inputSchema: z.object({}),
2160
+ outputSchema: z.object({
2161
+ totalLearnings: z.number(),
2162
+ successfulFixes: z.number(),
2163
+ selectorFixes: z.number(),
2164
+ timingFixes: z.number(),
2165
+ testsGenerated: z.number(),
2166
+ firstAttemptSuccessRate: z.number()
2167
+ })
2168
+ },
2169
+ async () => {
2170
+ const stats = getMemoryStats();
2171
+ const summary = `\u{1F4CA} **Estat\xEDsticas de Aprendizado**
2172
+
2173
+ - Total de aprendizados: ${stats.totalLearnings}
2174
+ - Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
2175
+ - Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
2176
+ - Corre\xE7\xF5es de timing: ${stats.timingFixes}
2177
+ - Testes gerados: ${stats.testsGenerated}
2178
+ - Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
2179
+
2180
+ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Use qa_auto para gerar testes e aprender com erros." : ""}`;
2181
+ return {
2182
+ content: [{ type: "text", text: summary }],
2183
+ structuredContent: stats
2184
+ };
2185
+ }
2186
+ );
2050
2187
  server.registerTool(
2051
2188
  "get_test_coverage",
2052
2189
  {
@@ -2129,6 +2266,198 @@ server.registerTool(
2129
2266
  };
2130
2267
  }
2131
2268
  );
2269
+ server.registerTool(
2270
+ "qa_auto",
2271
+ {
2272
+ title: "Modo aut\xF4nomo: gera, roda, corrige e aprende",
2273
+ description: "[AGENTE AUT\xD4NOMO] Loop completo: detecta projeto \u2192 gera teste \u2192 roda \u2192 se falhar: analisa, corrige, roda de novo \u2192 aprende com erros. Repete at\xE9 passar ou atingir max_retries.",
2274
+ inputSchema: z.object({
2275
+ request: z.string().describe("O que testar (ex: 'login flow', 'checkout', 'API /users')."),
2276
+ framework: z.enum([
2277
+ "cypress",
2278
+ "playwright",
2279
+ "webdriverio",
2280
+ "jest",
2281
+ "vitest",
2282
+ "mocha",
2283
+ "appium",
2284
+ "robot",
2285
+ "pytest"
2286
+ ]).optional().describe("Framework (detectado automaticamente se omitido)."),
2287
+ maxRetries: z.number().optional().describe("M\xE1ximo de tentativas de corre\xE7\xE3o. Default: 3.")
2288
+ }),
2289
+ outputSchema: z.object({
2290
+ ok: z.boolean(),
2291
+ testFilePath: z.string().optional(),
2292
+ attempts: z.number(),
2293
+ finalStatus: z.enum(["passed", "failed", "max_retries"]),
2294
+ learnings: z.array(z.object({ attempt: z.number(), action: z.string(), result: z.string() })).optional(),
2295
+ error: z.string().optional()
2296
+ })
2297
+ },
2298
+ async ({ request, framework, maxRetries = 3 }) => {
2299
+ const structure = detectProjectStructure();
2300
+ const fw = framework || structure.testFrameworks[0];
2301
+ if (!fw) {
2302
+ return {
2303
+ content: [{ type: "text", text: "Nenhum framework detectado. Configure testes primeiro." }],
2304
+ structuredContent: { ok: false, error: "No framework", finalStatus: "failed", attempts: 0 }
2305
+ };
2306
+ }
2307
+ const llm = resolveLLMProvider("simple");
2308
+ if (!llm.apiKey) {
2309
+ return {
2310
+ content: [{ type: "text", text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env" }],
2311
+ structuredContent: { ok: false, error: "No API key", finalStatus: "failed", attempts: 0 }
2312
+ };
2313
+ }
2314
+ const learnings = [];
2315
+ const memory = loadProjectMemory();
2316
+ const contextLines = [
2317
+ `Frameworks: ${structure.testFrameworks.join(", ")}`,
2318
+ `Pastas: ${structure.testDirs.join(", ")}`,
2319
+ memory.flows?.length ? `Fluxos: ${memory.flows.map((f) => f.name || f.id).join(", ")}` : ""
2320
+ ].filter(Boolean).join("\n");
2321
+ let testFilePath = null;
2322
+ let testContent = null;
2323
+ let attempt = 0;
2324
+ learnings.push({ attempt: 0, action: "detect_project", result: `${structure.testFrameworks.length} framework(s)` });
2325
+ for (attempt = 1; attempt <= maxRetries; attempt++) {
2326
+ learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
2327
+ const { provider, apiKey, baseUrl, model } = llm;
2328
+ const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
2329
+ const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
2330
+ ${memoryHints ? `
2331
+ Aprendizados anteriores (use como refer\xEAncia):
2332
+ ${memoryHints.slice(0, 1e3)}` : ""}
2333
+ Retorne SOMENTE o c\xF3digo, sem markdown.`;
2334
+ const userPrompt = `Contexto:
2335
+ ${contextLines}
2336
+
2337
+ Gere teste para: ${request}
2338
+ Framework: ${fw}`;
2339
+ try {
2340
+ let specContent = "";
2341
+ if (provider === "gemini") {
2342
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
2343
+ const res = await fetch(url, {
2344
+ method: "POST",
2345
+ headers: { "Content-Type": "application/json" },
2346
+ body: JSON.stringify({
2347
+ contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
2348
+ generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
2349
+ })
2350
+ });
2351
+ const data = await res.json();
2352
+ specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
2353
+ } else {
2354
+ const res = await fetch(`${baseUrl}/chat/completions`, {
2355
+ method: "POST",
2356
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
2357
+ body: JSON.stringify({
2358
+ model,
2359
+ messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
2360
+ temperature: 0.3,
2361
+ max_tokens: 4096
2362
+ })
2363
+ });
2364
+ const data = await res.json();
2365
+ specContent = data.choices?.[0]?.message?.content || "";
2366
+ }
2367
+ specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
2368
+ testContent = specContent;
2369
+ if (!testFilePath) {
2370
+ const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
2371
+ const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
2372
+ const safeName = fileName + ext;
2373
+ testFilePath = path.join(baseDir, safeName);
2374
+ if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
2375
+ }
2376
+ fs.writeFileSync(testFilePath, testContent, "utf8");
2377
+ learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
2378
+ learnings.push({ attempt, action: "run_tests", result: "executando..." });
2379
+ 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,
2382
+ stdio: ["inherit", "pipe", "pipe"],
2383
+ shell: process.platform === "win32"
2384
+ });
2385
+ let stdout = "", stderr = "";
2386
+ if (child.stdout) child.stdout.on("data", (d) => {
2387
+ stdout += d.toString();
2388
+ });
2389
+ if (child.stderr) child.stderr.on("data", (d) => {
2390
+ stderr += d.toString();
2391
+ });
2392
+ child.on("close", (code) => resolve({ code, output: [stdout, stderr].filter(Boolean).join("\n") }));
2393
+ });
2394
+ if (runResult.code === 0) {
2395
+ learnings.push({ attempt, action: "run_tests", result: "\u2705 passou" });
2396
+ saveProjectMemory({
2397
+ learnings: [{ type: "test_generated", request, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
2398
+ });
2399
+ return {
2400
+ content: [{ type: "text", text: `\u2705 Teste passou na tentativa ${attempt}!
2401
+
2402
+ Arquivo: ${testFilePath}
2403
+
2404
+ Aprendizados salvos.` }],
2405
+ structuredContent: { ok: true, testFilePath, attempts: attempt, finalStatus: "passed", learnings }
2406
+ };
2407
+ }
2408
+ learnings.push({ attempt, action: "run_tests", result: `\u274C falhou (exit ${runResult.code})` });
2409
+ if (attempt >= maxRetries) {
2410
+ learnings.push({ attempt, action: "max_retries", result: "limite atingido" });
2411
+ saveProjectMemory({
2412
+ learnings: [{ type: "test_generated", request, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
2413
+ });
2414
+ return {
2415
+ content: [{ type: "text", text: `\u274C Teste falhou ap\xF3s ${attempt} tentativa(s).
2416
+
2417
+ \xDAltimo erro:
2418
+ ${runResult.output.slice(0, 500)}` }],
2419
+ structuredContent: { ok: false, testFilePath, attempts: attempt, finalStatus: "max_retries", learnings }
2420
+ };
2421
+ }
2422
+ learnings.push({ attempt, action: "analyze_failures", result: "analisando..." });
2423
+ const flakyAnalysis = detectFlakyPatterns(runResult.output);
2424
+ const llmComplex = resolveLLMProvider("complex");
2425
+ const explainResult = await generateFailureExplanation(runResult.output, testFilePath);
2426
+ if (!explainResult.ok || !explainResult.structuredContent?.sugestaoCorrecao) {
2427
+ learnings.push({ attempt, action: "analyze_failures", result: "sem sugest\xE3o de corre\xE7\xE3o" });
2428
+ continue;
2429
+ }
2430
+ learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
2431
+ const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
2432
+ testContent = fixedCode;
2433
+ fs.writeFileSync(testFilePath, testContent, "utf8");
2434
+ learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
2435
+ if (flakyAnalysis.isLikelyFlaky) {
2436
+ saveProjectMemory({
2437
+ learnings: [{
2438
+ type: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
2439
+ request,
2440
+ framework: fw,
2441
+ fix: fixedCode.slice(0, 500),
2442
+ success: false,
2443
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2444
+ }]
2445
+ });
2446
+ }
2447
+ } catch (err) {
2448
+ learnings.push({ attempt, action: "error", result: err.message });
2449
+ return {
2450
+ content: [{ type: "text", text: `Erro na tentativa ${attempt}: ${err.message}` }],
2451
+ structuredContent: { ok: false, error: err.message, attempts: attempt, finalStatus: "failed", learnings }
2452
+ };
2453
+ }
2454
+ }
2455
+ return {
2456
+ content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).` }],
2457
+ structuredContent: { ok: false, testFilePath, attempts: maxRetries, finalStatus: "max_retries", learnings }
2458
+ };
2459
+ }
2460
+ );
2132
2461
  server.registerTool(
2133
2462
  "create_test_template",
2134
2463
  {
@@ -2179,16 +2508,23 @@ async function main() {
2179
2508
  const cmd = process.argv[2];
2180
2509
  if (cmd === "--help" || cmd === "-h") {
2181
2510
  console.log(`
2182
- mcp-lab-agent - MCP server para QA automation
2511
+ mcp-lab-agent - Agente aut\xF4nomo de QA que aprende com os pr\xF3prios erros
2183
2512
 
2184
2513
  USO:
2185
2514
  mcp-lab-agent [comando] # Sem comando: inicia servidor MCP
2186
2515
  mcp-lab-agent --help # Mostra esta ajuda
2187
2516
 
2188
2517
  COMANDOS CLI:
2189
- detect Detecta frameworks e estrutura do projeto (JSON)
2190
- route <tarefa> Sugere qual ferramenta usar (ex: route "rodar testes")
2191
- list Lista ferramentas MCP dispon\xEDveis
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
2192
2528
 
2193
2529
  INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
2194
2530
  Adicione ao ~/.cursor/mcp.json:
@@ -2206,7 +2542,25 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
2206
2542
  }
2207
2543
  if (cmd === "detect") {
2208
2544
  const structure = detectProjectStructure();
2209
- console.log(JSON.stringify(structure, null, 2));
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
+ }
2210
2564
  process.exit(0);
2211
2565
  }
2212
2566
  if (cmd === "list") {
@@ -2218,7 +2572,9 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
2218
2572
  const task = process.argv.slice(3).join(" ");
2219
2573
  const t = task.toLowerCase();
2220
2574
  let agent = "detection";
2221
- if (/rodar|executar|run|test|coverage|watch/i.test(t)) agent = "execution";
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";
2222
2578
  else if (/gerar|criar|escrever|generate|write|template/i.test(t)) agent = "generation";
2223
2579
  else if (/analisar|por que|falhou|sugerir|fix|selector/i.test(t)) agent = "analysis";
2224
2580
  else if (/browser|navegador|screenshot|network|console/i.test(t)) agent = "browser";
@@ -2228,6 +2584,191 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
2228
2584
  console.log(JSON.stringify({ suggestedAgent: agent, suggestedTools: a.tools, description: a.desc }, null, 2));
2229
2585
  process.exit(0);
2230
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
+ `);
2770
+ process.exit(0);
2771
+ }
2231
2772
  const transport = new StdioServerTransport();
2232
2773
  await server.connect(transport);
2233
2774
  }