mcp-lab-agent 2.1.4 → 2.1.10

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
@@ -6,8 +6,8 @@ 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
8
  import { spawn as spawn2 } from "child_process";
9
- import path5 from "path";
10
- import fs5 from "fs";
9
+ import path6 from "path";
10
+ import fs6 from "fs";
11
11
  import { fileURLToPath, pathToFileURL } from "url";
12
12
 
13
13
  // src/core/llm-router.js
@@ -42,6 +42,44 @@ function resolveLLMProvider(taskType = "simple") {
42
42
  // src/core/memory.js
43
43
  import path from "path";
44
44
  import fs from "fs";
45
+
46
+ // src/core/hub-client.js
47
+ var hubUrl = null;
48
+ function getHubUrl() {
49
+ if (hubUrl) return hubUrl;
50
+ const env = process.env.LEARNING_HUB_URL || process.env.QA_LAB_LEARNING_HUB_URL;
51
+ if (env) {
52
+ hubUrl = env.replace(/\/$/, "");
53
+ return hubUrl;
54
+ }
55
+ return null;
56
+ }
57
+ async function syncLearningsToHub(learnings) {
58
+ const baseUrl = getHubUrl();
59
+ if (!baseUrl) return;
60
+ const entries = Array.isArray(learnings) ? learnings : [learnings];
61
+ if (entries.length === 0) return;
62
+ const projectId = process.env.LEARNING_HUB_PROJECT_ID || process.cwd().split("/").pop() || "default";
63
+ const payload = entries.map((e) => ({
64
+ ...e,
65
+ projectId
66
+ }));
67
+ try {
68
+ const res = await fetch(`${baseUrl}/learning`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({ learnings: payload })
72
+ });
73
+ if (!res.ok) {
74
+ const txt = await res.text();
75
+ console.warn(`[learning-hub] POST /learning failed ${res.status}: ${txt}`);
76
+ }
77
+ } catch (err) {
78
+ console.warn("[learning-hub] sync failed:", err.message);
79
+ }
80
+ }
81
+
82
+ // src/core/memory.js
45
83
  var PROJECT_ROOT = process.cwd();
46
84
  var MEMORY_FILE = path.join(PROJECT_ROOT, ".qa-lab-memory.json");
47
85
  var FLOWS_CONFIG_FILE2 = path.join(PROJECT_ROOT, "qa-lab-flows.json");
@@ -74,6 +112,8 @@ function saveProjectMemory(updates) {
74
112
  data.learnings = data.learnings || [];
75
113
  data.learnings.push(...updates.learnings);
76
114
  if (data.learnings.length > 200) data.learnings = data.learnings.slice(-150);
115
+ syncLearningsToHub(updates.learnings).catch(() => {
116
+ });
77
117
  }
78
118
  if (updates.execution) {
79
119
  data.executions = data.executions || [];
@@ -85,12 +125,17 @@ function saveProjectMemory(updates) {
85
125
  } catch {
86
126
  }
87
127
  }
128
+ var LEARNING_TYPES = ["selector_fix", "timing_fix", "element_not_rendered", "element_not_visible", "element_stale", "mobile_mapping_invisible"];
88
129
  function getMemoryStats() {
89
130
  const memory = loadProjectMemory();
90
131
  const learnings = memory.learnings || [];
91
132
  const successfulFixes = learnings.filter((l) => l.success);
92
133
  const selectorFixes = learnings.filter((l) => l.type === "selector_fix");
93
134
  const timingFixes = learnings.filter((l) => l.type === "timing_fix");
135
+ const byLearningType = {};
136
+ for (const t of LEARNING_TYPES) {
137
+ byLearningType[t] = learnings.filter((l) => l.type === t).length;
138
+ }
94
139
  const totalTests = learnings.filter((l) => l.type === "test_generated").length;
95
140
  const firstAttemptSuccess = learnings.filter((l) => l.type === "test_generated" && l.passedFirstTime).length;
96
141
  return {
@@ -98,6 +143,7 @@ function getMemoryStats() {
98
143
  successfulFixes: successfulFixes.length,
99
144
  selectorFixes: selectorFixes.length,
100
145
  timingFixes: timingFixes.length,
146
+ byLearningType,
101
147
  testsGenerated: totalTests,
102
148
  firstAttemptSuccessRate: totalTests > 0 ? Math.round(firstAttemptSuccess / totalTests * 100) : 0
103
149
  };
@@ -141,6 +187,110 @@ var FLAKY_PATTERNS = [
141
187
  { name: "network", regex: /ECONNREFUSED|network|fetch|axios|request failed|404|500/i, suggestion: "Mocke APIs ou garanta que o backend esteja rodando. Use retry ou intercept." },
142
188
  { name: "shared_state", regex: /state|cleanup|beforeEach|afterEach|isolation/i, suggestion: "Garanta beforeEach/afterEach para resetar estado. Evite vari\xE1veis globais compartilhadas." }
143
189
  ];
190
+ var FAILURE_ANALYSIS_PATTERNS = [
191
+ {
192
+ name: "element_not_rendered",
193
+ regex: /timeout|not found|element not found|no such element|element.*not.*in.*dom|waiting for/i,
194
+ oQueAconteceu: "O elemento ainda n\xE3o foi renderizado no DOM quando o teste tentou interagir. Pode ser carregamento ass\xEDncrono, lazy load ou anima\xE7\xE3o.",
195
+ lesson: `Espere o elemento estar dispon\xEDvel ANTES de interagir:
196
+ - Playwright: await element.waitFor({ state: 'attached' }) ou waitForSelector
197
+ - Cypress: cy.get(sel).should('exist') antes de clicar
198
+ - WDIO/Appium: $(sel).waitForDisplayed() ou waitForExist({ timeout: 10000 })
199
+ - Use waits inteligentes: waitForDisplayed, waitForClickable, waitForExist`,
200
+ learningType: "element_not_rendered"
201
+ },
202
+ {
203
+ name: "element_not_visible",
204
+ regex: /element.*not.*visible|not visible|is not visible|element is not displayed|hidden|display.*none|off.?screen/i,
205
+ oQueAconteceu: "O elemento existe no DOM mas n\xE3o est\xE1 vis\xEDvel (display:none, off-screen, opacity:0 ou ainda carregando).",
206
+ lesson: `Verifique visibilidade antes de interagir:
207
+ - Playwright: waitFor({ state: 'visible' })
208
+ - Cypress: .should('be.visible') antes de click
209
+ - Appium/WDIO: waitForDisplayed() ou isDisplayed()
210
+ - Adicione wait expl\xEDcito: elemento pode estar em anima\xE7\xE3o ou carregando`,
211
+ learningType: "element_not_visible"
212
+ },
213
+ {
214
+ name: "element_stale",
215
+ regex: /stale element|stale element reference|element.*no longer attached/i,
216
+ oQueAconteceu: "O elemento foi encontrado mas a p\xE1gina/DOM mudou antes da intera\xE7\xE3o (elemento ficou obsoleto).",
217
+ lesson: `Re-localize o elemento antes de cada a\xE7\xE3o:
218
+ - Evite guardar refer\xEAncia: busque novamente antes de clicar
219
+ - Use waits que revalidam: cy.get().first().click() com retry
220
+ - Em listas din\xE2micas: espere estabiliza\xE7\xE3o antes de interagir`,
221
+ learningType: "element_stale"
222
+ },
223
+ {
224
+ name: "mobile_mapping_invisible",
225
+ regex: /element not found|selector|Unable to find|no such element/i,
226
+ oQueAconteceu: "Em mobile: o mapeamento ficou invis\xEDvel ou os seletores n\xE3o est\xE3o estruturados. Pode ser estrutura do c\xF3digo ou seletor incorreto.",
227
+ lesson: `Em testes mobile (Appium/Detox), SEMPRE:
228
+ - Mapeamento VIS\xCDVEL: const ELEMENTS = { btn: '~id' }; $(ELEMENTS.btn).click()
229
+ - Antes de clicar: $(sel).waitForDisplayed({ timeout: 10000 })
230
+ - Ao final: expect(await $(sel).isDisplayed()).toBe(true) \u2014 valida\xE7\xE3o expl\xEDcita para o usu\xE1rio entender que houve valida\xE7\xE3o`,
231
+ learningType: "mobile_mapping_invisible",
232
+ mobileOnly: true
233
+ },
234
+ {
235
+ name: "selector",
236
+ regex: /selector|locator|element not found|Unable to find/i,
237
+ oQueAconteceu: "O seletor n\xE3o encontrou o elemento. Pode ser seletor incorreto, mudan\xE7a de UI ou elemento em outro contexto (iframe, shadow DOM).",
238
+ lesson: "Use seletores est\xE1veis: data-testid, role+name, accessibility-id. Evite classes CSS din\xE2micas. Priorize: data-testid > role > texto vis\xEDvel.",
239
+ learningType: "selector_fix"
240
+ },
241
+ {
242
+ name: "timing",
243
+ regex: /timeout|timed out|exceeded|slow/i,
244
+ oQueAconteceu: "O teste excedeu o tempo de espera. O elemento pode demorar para aparecer ou h\xE1 race condition.",
245
+ lesson: "Adicione wait expl\xEDcito antes de interagir. Aumente timeout se necess\xE1rio. Use waitForDisplayed/waitForSelector.",
246
+ learningType: "timing_fix"
247
+ }
248
+ ];
249
+ function oneLineFailureSummary(runOutput, framework = "", oQueAconteceu = "", sugestaoCorrecao = "") {
250
+ const p = inferFailurePattern(runOutput, framework);
251
+ const causa = oQueAconteceu || p?.oQueAconteceu || "erro desconhecido";
252
+ const solucao = sugestaoCorrecao || (p ? p.lesson.split("\n")[0].replace(/^-\s*/, "") : "");
253
+ const tipo = p?.name || "geral";
254
+ if (solucao) {
255
+ return `Falhou porque ${causa.slice(0, 80)}${causa.length > 80 ? "\u2026" : ""} (${tipo}). Solu\xE7\xE3o: ${solucao.slice(0, 100)}${solucao.length > 100 ? "\u2026" : ""}`;
256
+ }
257
+ return `Falhou porque ${causa.slice(0, 120)}${causa.length > 120 ? "\u2026" : ""} (${tipo}).`;
258
+ }
259
+ function inferFailurePattern(runOutput, framework = "") {
260
+ const output = (runOutput || "").toLowerCase();
261
+ for (const p of FAILURE_ANALYSIS_PATTERNS) {
262
+ if (p.mobileOnly && !/appium|detox/i.test(framework)) continue;
263
+ if (p.regex.test(output)) return p;
264
+ }
265
+ return null;
266
+ }
267
+ var MOBILE_SELECTOR_HIERARCHY = `HIERARQUIA DE SELETORES MOBILE (\xFAnica e inovadora):
268
+ 1. id: ~accessibility-id, testID \u2014 prioridade m\xE1xima, sem\xE2ntico e est\xE1vel
269
+ 2. XPath relacional: \xE2ncora est\xE1vel + eixos + TIPO ESPEC\xCDFICO (android.widget.Button, XCUIElementTypeButton). NUNCA use * \u2014 quebra por timing e m\xFAltiplos matches. Ex: //android.widget.LinearLayout[@resource-id='login_form']/descendant::android.widget.Button[@text='Entrar']. Evite XPath por \xEDndice (//Button[3])
270
+ 3. resource-id: id=com.app:id/btn \u2014 fallback`;
271
+ var MOBILE_MAPPING_LESSON = `Em testes mobile (Appium/Detox), SEMPRE inclua o mapeamento de elementos de forma VIS\xCDVEL e estruturada no c\xF3digo:
272
+ - Use constantes ou Page Object no TOPO do spec: const ELEMENTS = { loginBtn: '~btn_login', ... };
273
+ - No teste: $(ELEMENTS.loginBtn).click();
274
+ - Nunca deixe seletores "invis\xEDveis" (hardcoded inline repetidos). Isso dificulta manuten\xE7\xE3o e causa falhas.
275
+ - Hierarquia: id > XPath relacional (\xE2ncora + eixos + tipo espec\xEDfico: android.widget.Button) > resource-id. Evite * e \xEDndice.`;
276
+ var UNIVERSAL_TEST_PRACTICES = `PR\xC1TICAS OBRIGAT\xD3RIAS em todo teste gerado:
277
+ 1. Esperas inteligentes: ANTES de interagir, verifique que o elemento est\xE1 dispon\xEDvel (waitForDisplayed, waitForExist, waitForSelector)
278
+ 2. Valida\xE7\xE3o no final: SEMPRE adicione um expect/assert ao final para o usu\xE1rio entender que houve valida\xE7\xE3o (ex: expect(element).toBeVisible() ou cy.get(sel).should('be.visible'))
279
+ 3. N\xE3o assuma que o elemento est\xE1 pronto: elemento pode n\xE3o estar renderizado, vis\xEDvel ou dispon\xEDvel \u2014 use waits expl\xEDcitos`;
280
+ function formatLearnedMessageForUser({ pattern, fixSummary, runOutput, framework }) {
281
+ const p = pattern || (runOutput ? inferFailurePattern(runOutput, framework) : null);
282
+ const oQueAconteceu = p?.oQueAconteceu || "O teste falhou por um problema de elemento ou timing.";
283
+ const oQueFiz = fixSummary || (p ? `Apliquei a corre\xE7\xE3o para esse tipo de falha: ${p.name}.` : "Ajustei o c\xF3digo.");
284
+ return `**Entendi o erro e apliquei a corre\xE7\xE3o**
285
+
286
+ **O que aconteceu:** ${oQueAconteceu}
287
+
288
+ **O que fiz:** ${oQueFiz}
289
+
290
+ **O que aprendi:** Salvei esse cen\xE1rio no meu hist\xF3rico. Nas pr\xF3ximas gera\xE7\xF5es, vou aplicar as pr\xE1ticas corretas (waits inteligentes, valida\xE7\xE3o final) desde o in\xEDcio.
291
+
292
+ Use \`mcp-lab-agent stats\` ou \`get_learning_report\` para ver a evolu\xE7\xE3o dos aprendizados.`;
293
+ }
144
294
  function detectFlakyPatterns(runOutput) {
145
295
  const detected = [];
146
296
  for (const p of FLAKY_PATTERNS) {
@@ -214,6 +364,9 @@ function detectProjectStructure() {
214
364
  structure.hasTests = true;
215
365
  structure.hasMobile = true;
216
366
  }
367
+ if (deps["react-native"]) {
368
+ structure.hasMobile = true;
369
+ }
217
370
  if (deps.supertest) {
218
371
  structure.testFrameworks.push("supertest");
219
372
  structure.hasTests = true;
@@ -370,6 +523,41 @@ function detectProjectStructure() {
370
523
  }
371
524
  }
372
525
  }
526
+ const hints = [];
527
+ if (structure.hasMobile) hints.push("mobile");
528
+ if (structure.testFrameworks.includes("appium")) hints.push("appium");
529
+ if (structure.testFrameworks.includes("detox")) hints.push("detox");
530
+ const pkg = structure.packageJson || {};
531
+ const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
532
+ if (allDeps["react-native"]) hints.push("react-native");
533
+ const webFrameworks = ["cypress", "playwright", "webdriverio", "selenium", "puppeteer", "testcafe"];
534
+ const hasWebFrameworks = structure.testFrameworks.some((f) => webFrameworks.includes(f));
535
+ if (hasWebFrameworks) hints.push("web");
536
+ if (structure.testDirs.includes("mobile")) hints.push("mobile-dir");
537
+ const configPath = path2.join(PROJECT_ROOT2, "qa-lab-agent.config.json");
538
+ if (fs2.existsSync(configPath)) {
539
+ try {
540
+ const cfg = JSON.parse(fs2.readFileSync(configPath, "utf8"));
541
+ const customDirs = cfg.testDirs || cfg.qa?.testDirs;
542
+ if (Array.isArray(customDirs)) {
543
+ for (const dir of customDirs) {
544
+ const d = String(dir).trim();
545
+ if (d && !structure.testDirs.includes(d)) {
546
+ const fullPath = path2.join(PROJECT_ROOT2, d);
547
+ if (fs2.existsSync(fullPath) && fs2.statSync(fullPath).isDirectory()) {
548
+ structure.testDirs.push(d);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ } catch {
554
+ }
555
+ }
556
+ let environment = "web";
557
+ if (structure.hasMobile && !hasWebFrameworks) environment = "mobile";
558
+ else if (structure.hasMobile && hasWebFrameworks) environment = "both";
559
+ structure.environment = environment;
560
+ structure.environmentHints = [...new Set(hints)];
373
561
  return structure;
374
562
  }
375
563
  var UNIVERSAL_TEST_PATTERNS = [
@@ -450,6 +638,61 @@ function matchesFramework(inferred, requested) {
450
638
  if (inferred === requested) return true;
451
639
  return aliases[inferred]?.includes(requested);
452
640
  }
641
+ function detectDeviceConfig(structure) {
642
+ const result = { device: null, configuration: null, platform: null, envOverrides: {} };
643
+ if (!structure.hasMobile) return result;
644
+ const configPath = path2.join(PROJECT_ROOT2, "qa-lab-agent.config.json");
645
+ if (fs2.existsSync(configPath)) {
646
+ try {
647
+ const cfg = JSON.parse(fs2.readFileSync(configPath, "utf8"));
648
+ const deviceCfg = cfg.device || cfg.mobile || cfg.appium || cfg.detox;
649
+ if (deviceCfg) {
650
+ result.device = deviceCfg.deviceName || deviceCfg.device || deviceCfg.udid;
651
+ result.configuration = deviceCfg.configuration || deviceCfg.config;
652
+ result.platform = deviceCfg.platformName || deviceCfg.platform;
653
+ if (deviceCfg.udid) result.envOverrides.APPIUM_UDID = deviceCfg.udid;
654
+ if (deviceCfg.deviceName) result.envOverrides.APPIUM_DEVICE_NAME = deviceCfg.deviceName;
655
+ }
656
+ } catch {
657
+ }
658
+ }
659
+ if (process.env.DETOX_CONFIGURATION) result.configuration = process.env.DETOX_CONFIGURATION;
660
+ if (process.env.APPIUM_UDID) result.envOverrides.APPIUM_UDID = process.env.APPIUM_UDID;
661
+ if (process.env.APPIUM_DEVICE_NAME) result.envOverrides.APPIUM_DEVICE_NAME = process.env.APPIUM_DEVICE_NAME;
662
+ const detoxPath = path2.join(PROJECT_ROOT2, ".detoxrc.js");
663
+ if (fs2.existsSync(detoxPath) && !result.configuration) {
664
+ try {
665
+ const content = fs2.readFileSync(detoxPath, "utf8");
666
+ const configMatch = content.match(/configurations:\s*\{([^}]+)\}/s);
667
+ if (configMatch) {
668
+ const firstConfig = configMatch[1].match(/"([^"]+)":\s*\{/);
669
+ if (firstConfig) result.configuration = firstConfig[1];
670
+ }
671
+ } catch {
672
+ }
673
+ }
674
+ const wdioPaths = ["wdio.conf.js", "wdio.conf.cjs", "wdio.conf.mjs", "wdio.conf.ts"];
675
+ for (const name of wdioPaths) {
676
+ const wdioPath = path2.join(PROJECT_ROOT2, name);
677
+ if (fs2.existsSync(wdioPath) && !result.device) {
678
+ try {
679
+ const content = fs2.readFileSync(wdioPath, "utf8");
680
+ const capMatch = content.match(/capabilities:\s*\[([\s\S]*?)\]/);
681
+ if (capMatch) {
682
+ const deviceMatch = capMatch[1].match(/deviceName:\s*['"]([^'"]+)['"]/);
683
+ const udidMatch = capMatch[1].match(/udid:\s*['"]([^'"]+)['"]/);
684
+ const platformMatch = capMatch[1].match(/platformName:\s*['"]([^'"]+)['"]/);
685
+ if (deviceMatch) result.device = deviceMatch[1];
686
+ if (udidMatch) result.envOverrides.APPIUM_UDID = udidMatch[1];
687
+ if (platformMatch) result.platform = platformMatch[1];
688
+ }
689
+ } catch {
690
+ }
691
+ break;
692
+ }
693
+ }
694
+ return result;
695
+ }
453
696
  function getFrameworkCwd(structure, preferredDirs) {
454
697
  for (const dir of preferredDirs) {
455
698
  if (structure.testDirs.includes(dir)) {
@@ -488,11 +731,90 @@ function analyzeCodeRisks() {
488
731
  });
489
732
  }
490
733
 
491
- // src/core/tool-helpers.js
734
+ // src/core/llm-call.js
492
735
  import path3 from "path";
493
736
  import fs3 from "fs";
494
737
  var PROJECT_ROOT3 = process.cwd();
495
- var METRICS_FILE = path3.join(PROJECT_ROOT3, ".qa-lab-metrics.json");
738
+ async function callLlm(provider, apiKey, baseUrl, model, systemPrompt, userPrompt) {
739
+ if (provider === "gemini") {
740
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
741
+ const res2 = await fetch(url, {
742
+ method: "POST",
743
+ headers: { "Content-Type": "application/json" },
744
+ body: JSON.stringify({
745
+ contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
746
+ generationConfig: { temperature: 0.2, maxOutputTokens: 4096 }
747
+ })
748
+ });
749
+ const data2 = await res2.json();
750
+ return data2.candidates?.[0]?.content?.parts?.[0]?.text || "";
751
+ }
752
+ const res = await fetch(`${baseUrl}/chat/completions`, {
753
+ method: "POST",
754
+ headers: {
755
+ "Content-Type": "application/json",
756
+ Authorization: `Bearer ${apiKey}`
757
+ },
758
+ body: JSON.stringify({
759
+ model,
760
+ messages: [
761
+ { role: "system", content: systemPrompt },
762
+ { role: "user", content: userPrompt }
763
+ ],
764
+ temperature: 0.2,
765
+ max_tokens: 4096
766
+ })
767
+ });
768
+ const data = await res.json();
769
+ return data.choices?.[0]?.message?.content || "";
770
+ }
771
+ async function applySelectorFixAndRetry(testFilePath, errorOutput, framework) {
772
+ const structure = detectProjectStructure();
773
+ const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
774
+ const fullPath = path3.join(PROJECT_ROOT3, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
775
+ if (!fs3.existsSync(fullPath)) return { applied: false };
776
+ let testCode = "";
777
+ try {
778
+ testCode = fs3.readFileSync(fullPath, "utf8");
779
+ } catch {
780
+ return { applied: false };
781
+ }
782
+ const llm = resolveLLMProvider("complex");
783
+ if (!llm.apiKey) return { applied: false };
784
+ const { provider, apiKey, baseUrl, model } = llm;
785
+ const systemPrompt = `Voc\xEA \xE9 um especialista em testes E2E. O teste falhou porque um seletor n\xE3o encontrou o elemento.
786
+ Retorne APENAS em JSON (sem markdown) com a chave:
787
+ - codigoCorrigido: string (o ARQUIVO COMPLETO do teste corrigido, com imports e toda a estrutura. Substitua o seletor quebrado por um mais resiliente: data-testid, role, ~accessibility-id, ou XPath relacional com tipo espec\xEDfico.)
788
+
789
+ Framework: ${fw}. Priorize seletores est\xE1veis.`;
790
+ const userPrompt = `Output do erro:
791
+ ---
792
+ ${(errorOutput || "").slice(0, 8e3)}
793
+ ---
794
+
795
+ C\xF3digo atual:
796
+ ---
797
+ ${testCode.slice(0, 6e3)}
798
+ ---`;
799
+ try {
800
+ let raw = await callLlm(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
801
+ raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
802
+ const data = JSON.parse(raw);
803
+ const fixed = (data.codigoCorrigido || "").trim();
804
+ if (fixed.length > 50 && /describe|it\(|test\(|cy\.|page\.|\$\(/.test(fixed)) {
805
+ fs3.writeFileSync(fullPath, fixed, "utf8");
806
+ return { applied: true };
807
+ }
808
+ } catch {
809
+ }
810
+ return { applied: false };
811
+ }
812
+
813
+ // src/core/tool-helpers.js
814
+ import path4 from "path";
815
+ import fs4 from "fs";
816
+ var PROJECT_ROOT4 = process.cwd();
817
+ var METRICS_FILE = path4.join(PROJECT_ROOT4, ".qa-lab-metrics.json");
496
818
  function parseTestRunResult(runOutput, exitCode) {
497
819
  let passed = 0;
498
820
  let failed = 0;
@@ -506,8 +828,8 @@ function parseTestRunResult(runOutput, exitCode) {
506
828
  function recordMetricEvent(event) {
507
829
  try {
508
830
  let data = {};
509
- if (fs3.existsSync(METRICS_FILE)) {
510
- const raw = fs3.readFileSync(METRICS_FILE, "utf8");
831
+ if (fs4.existsSync(METRICS_FILE)) {
832
+ const raw = fs4.readFileSync(METRICS_FILE, "utf8");
511
833
  try {
512
834
  data = JSON.parse(raw);
513
835
  } catch {
@@ -517,7 +839,7 @@ function recordMetricEvent(event) {
517
839
  data.events.push({ ...event, timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString() });
518
840
  data.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
519
841
  if (data.events.length > 500) data.events = data.events.slice(-400);
520
- fs3.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
842
+ fs4.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
521
843
  } catch {
522
844
  }
523
845
  }
@@ -535,36 +857,12 @@ function extractFailuresFromOutput(runOutput) {
535
857
  }
536
858
  return failures.slice(0, 20);
537
859
  }
538
- function generateFailureExplanation(testCode, runOutput, memory = {}) {
539
- const lines = [];
540
- lines.push("# An\xE1lise de Falha\n");
541
- lines.push("## C\xF3digo do Teste");
542
- lines.push("```");
543
- lines.push(testCode.slice(0, 2e3));
544
- lines.push("```\n");
545
- lines.push("## Output da Execu\xE7\xE3o");
546
- lines.push("```");
547
- lines.push(runOutput.slice(0, 2e3));
548
- lines.push("```\n");
549
- if (memory.learnings && memory.learnings.length > 0) {
550
- lines.push("## Aprendizados Anteriores (\xFAltimos 5)");
551
- memory.learnings.slice(-5).forEach((l) => {
552
- lines.push(`- **${l.type}**: ${l.description || "N/A"}`);
553
- });
554
- lines.push("");
555
- }
556
- lines.push("## Sua Tarefa");
557
- lines.push("1. Identifique a causa raiz da falha");
558
- lines.push("2. Sugira uma corre\xE7\xE3o espec\xEDfica");
559
- lines.push("3. Explique por que essa corre\xE7\xE3o deve funcionar");
560
- return lines.join("\n");
561
- }
562
860
 
563
861
  // src/cli/commands.js
564
- import path4 from "path";
565
- import fs4 from "fs";
862
+ import path5 from "path";
863
+ import fs5 from "fs";
566
864
  import { spawn } from "child_process";
567
- var PROJECT_ROOT4 = process.cwd();
865
+ var PROJECT_ROOT5 = process.cwd();
568
866
  var QA_AGENTS = {
569
867
  autonomous: { desc: "Modo aut\xF4nomo: gera, testa, corrige e aprende", tools: ["qa_auto"] },
570
868
  detection: { desc: "Detecta estrutura, frameworks, testes", tools: ["detect_project", "read_project", "list_test_files"] },
@@ -574,13 +872,13 @@ var QA_AGENTS = {
574
872
  browser: { desc: "Browser mode: screenshots, network, console", tools: ["web_eval_browser"] },
575
873
  reporting: { desc: "Relat\xF3rios e m\xE9tricas", tools: ["create_bug_report", "get_business_metrics"] },
576
874
  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"] },
577
- learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats"] },
875
+ learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats", "get_learning_report"] },
578
876
  maintenance: { desc: "Linter, deps, an\xE1lise de c\xF3digo", tools: ["run_linter", "install_dependencies"] }
579
877
  };
580
878
  function getExtensionAndBaseDir(fw, structure) {
581
879
  const extMap = { cypress: ".cy.js", playwright: ".spec.js", jest: ".test.js", vitest: ".test.js", robot: ".robot", pytest: ".py" };
582
880
  const ext = extMap[fw] || ".spec.js";
583
- const baseDir = structure.testDirs[0] ? path4.join(PROJECT_ROOT4, structure.testDirs[0]) : path4.join(PROJECT_ROOT4, "tests");
881
+ const baseDir = structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : path5.join(PROJECT_ROOT5, "tests");
584
882
  return { ext, baseDir };
585
883
  }
586
884
  async function handleCLI() {
@@ -594,20 +892,29 @@ USO:
594
892
  mcp-lab-agent --help # Mostra esta ajuda
595
893
 
596
894
  COMANDOS CLI:
597
- slack-bot Inicia o Slack Bot (QA via @mention) - sem precisar clonar o repo
895
+ slack-bot Inicia o Slack Bot (QA via @mention)
896
+ learning-hub Inicia o Learning Hub (API + Dashboard em http://localhost:3847)
598
897
  analyze An\xE1lise completa: executa, analisa estabilidade, prev\xEA riscos e recomenda a\xE7\xF5es
599
898
  auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
600
- stats Mostra estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
899
+ stats Estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
900
+ report [--full] Relat\xF3rio de evolu\xE7\xE3o e aprendizado (--full = completo com recomenda\xE7\xF5es)
901
+ metrics-report [--json] [--output FILE] [path1 path2 ...] Relat\xF3rio de m\xE9tricas (m\xE9todo, resultado). Sem paths = projeto atual.
902
+ flaky-report [--runs N] [--spec FILE] [--output FILE] Detecta testes flaky: roda N vezes (default 3), identifica intermit\xEAncia e causa prov\xE1vel
903
+ run [spec] [--device NAME] [--no-auto-fix] Roda testes: detecta device, executa e aplica auto-fix de seletor se falhar
601
904
  detect [--json] Detecta frameworks e estrutura
602
905
  route <tarefa> Sugere qual ferramenta usar
603
906
  list Lista ferramentas MCP dispon\xEDveis
604
907
 
605
908
  EXEMPLOS:
606
- mcp-lab-agent slack-bot # Slack Bot (configure em ~/.cursor/mcp.json)
909
+ mcp-lab-agent slack-bot # Slack Bot
910
+ mcp-lab-agent learning-hub # Learning Hub (API + Dashboard)
607
911
  npx mcp-lab-agent slack-bot # Usar sem instalar (sem clonar o projeto)
608
912
  mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
609
913
  mcp-lab-agent auto "login flow" --max-retries 5
610
914
  mcp-lab-agent stats
915
+ mcp-lab-agent flaky-report --runs 5 --output flaky.md
916
+ mcp-lab-agent run specs/login.spec.js
917
+ mcp-lab-agent run specs/login.spec.js --device iPhone_15
611
918
  mcp-lab-agent detect --json
612
919
 
613
920
  INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
@@ -675,31 +982,529 @@ AMBIENTES CORPORATIVOS (APIs bloqueadas):
675
982
  console.log(JSON.stringify({ suggestedAgent: agent, suggestedTools: a.tools, description: a.desc }, null, 2));
676
983
  return true;
677
984
  }
678
- if (cmd === "auto") {
679
- await handleAutoCommand();
680
- return true;
985
+ if (cmd === "auto") {
986
+ await handleAutoCommand();
987
+ return true;
988
+ }
989
+ if (cmd === "stats") {
990
+ const stats = getMemoryStats();
991
+ const byType = stats.byLearningType || {};
992
+ const byTypeLines = Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => ` ${t}: ${v}`).join("\n");
993
+ console.log(`
994
+ \u{1F4CA} Estat\xEDsticas de Aprendizado
995
+
996
+ Total de aprendizados: ${stats.totalLearnings}
997
+ Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
998
+ Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
999
+ Corre\xE7\xF5es de timing: ${stats.timingFixes}
1000
+ Testes gerados: ${stats.testsGenerated}
1001
+ Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
1002
+ ${byTypeLines ? `
1003
+ Por tipo:
1004
+ ${byTypeLines}` : ""}
1005
+
1006
+ ${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." : ""}
1007
+ `);
1008
+ return true;
1009
+ }
1010
+ if (cmd === "report") {
1011
+ const memory = loadProjectMemory();
1012
+ const learnings = memory.learnings || [];
1013
+ const stats = getMemoryStats();
1014
+ const byType = stats.byLearningType || {};
1015
+ const format = process.argv.includes("--full") ? "full" : "summary";
1016
+ const recommendations = [];
1017
+ if (byType.element_not_rendered > 0 || byType.element_not_visible > 0) {
1018
+ recommendations.push("Use waits expl\xEDcitos (waitForSelector, waitForDisplayed) ANTES de interagir com elementos.");
1019
+ }
1020
+ if (byType.timing_fix > 0 || byType.element_stale > 0) {
1021
+ recommendations.push("Aumente timeouts e use re-localiza\xE7\xE3o de elementos em listas din\xE2micas.");
1022
+ }
1023
+ if (byType.selector_fix > 0 || byType.mobile_mapping_invisible > 0) {
1024
+ recommendations.push("Priorize data-testid, role e seletores est\xE1veis; em mobile, use mapeamento vis\xEDvel no topo do spec.");
1025
+ }
1026
+ if (stats.firstAttemptSuccessRate < 70 && stats.testsGenerated > 0) {
1027
+ recommendations.push("Aplique waits inteligentes + assert final em cada teste gerado.");
1028
+ }
1029
+ const byTypeStr = Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => ` - ${t}: ${v}`).join("\n");
1030
+ console.log(`
1031
+ \u{1F4C8} Relat\xF3rio de Evolu\xE7\xE3o e Aprendizado
1032
+
1033
+ Resumo por tipo:
1034
+ ${byTypeStr || " Nenhum aprendizado por tipo ainda"}
1035
+
1036
+ M\xE9tricas gerais:
1037
+ Total de aprendizados: ${stats.totalLearnings}
1038
+ Taxa de sucesso (1\xAA tentativa): ${stats.firstAttemptSuccessRate}%
1039
+ Testes gerados: ${stats.testsGenerated}
1040
+ ${format === "full" && recommendations.length > 0 ? `
1041
+ Recomenda\xE7\xF5es para aprimorar o c\xF3digo:
1042
+ ${recommendations.map((r) => ` \u2022 ${r}`).join("\n")}` : ""}
1043
+ `);
1044
+ return true;
1045
+ }
1046
+ if (cmd === "analyze") {
1047
+ await handleAnalyzeCommand();
1048
+ return true;
1049
+ }
1050
+ if (cmd === "metrics-report") {
1051
+ await handleMetricsReportCommand();
1052
+ return true;
1053
+ }
1054
+ if (cmd === "flaky-report") {
1055
+ await handleFlakyReportCommand();
1056
+ return true;
1057
+ }
1058
+ if (cmd === "run") {
1059
+ await handleRunCommand();
1060
+ return true;
1061
+ }
1062
+ return false;
1063
+ }
1064
+ async function handleMetricsReportCommand() {
1065
+ const argv = process.argv.slice(2);
1066
+ const jsonOnly = argv.includes("--json");
1067
+ const outputIdx = argv.indexOf("--output");
1068
+ const outputFile = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : null;
1069
+ const paths = argv.filter((a) => {
1070
+ if (a.startsWith("--") || a === "metrics-report") return false;
1071
+ if (outputIdx !== -1 && a === argv[outputIdx + 1]) return false;
1072
+ return true;
1073
+ });
1074
+ const projectDirs = paths.length > 0 ? paths : [PROJECT_ROOT5];
1075
+ const reports = [];
1076
+ for (const dir of projectDirs) {
1077
+ const resolved = path5.resolve(dir);
1078
+ if (!fs5.existsSync(resolved)) {
1079
+ console.warn(`\u26A0\uFE0F Diret\xF3rio n\xE3o encontrado: ${dir}`);
1080
+ continue;
1081
+ }
1082
+ const memoryPath = path5.join(resolved, ".qa-lab-memory.json");
1083
+ const metricsPath = path5.join(resolved, ".qa-lab-metrics.json");
1084
+ let memory = {};
1085
+ let metrics = { events: [] };
1086
+ if (fs5.existsSync(memoryPath)) {
1087
+ try {
1088
+ memory = JSON.parse(fs5.readFileSync(memoryPath, "utf8"));
1089
+ } catch {
1090
+ }
1091
+ }
1092
+ if (fs5.existsSync(metricsPath)) {
1093
+ try {
1094
+ metrics = JSON.parse(fs5.readFileSync(metricsPath, "utf8"));
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ const events = metrics.events || [];
1099
+ const executions = memory.executions || [];
1100
+ const learnings = memory.learnings || [];
1101
+ const byEventType = {};
1102
+ events.forEach((e) => {
1103
+ const t = e.type || "unknown";
1104
+ byEventType[t] = (byEventType[t] || 0) + 1;
1105
+ });
1106
+ const testRuns = events.filter((e) => e.type === "test_run");
1107
+ const testRunPassed = testRuns.filter((e) => e.exitCode === 0).length;
1108
+ const testRunFailed = testRuns.filter((e) => e.exitCode !== 0).length;
1109
+ const byFramework = {};
1110
+ testRuns.forEach((e) => {
1111
+ const f = e.framework || "unknown";
1112
+ byFramework[f] = byFramework[f] || { total: 0, passed: 0, failed: 0 };
1113
+ byFramework[f].total++;
1114
+ if (e.exitCode === 0) byFramework[f].passed++;
1115
+ else byFramework[f].failed++;
1116
+ });
1117
+ const byLearningType = {};
1118
+ learnings.forEach((l) => {
1119
+ const t = l.type || "unknown";
1120
+ byLearningType[t] = byLearningType[t] || { total: 0, success: 0 };
1121
+ byLearningType[t].total++;
1122
+ if (l.success) byLearningType[t].success++;
1123
+ });
1124
+ const execByFramework = {};
1125
+ executions.forEach((e) => {
1126
+ const f = e.framework || "unknown";
1127
+ execByFramework[f] = execByFramework[f] || { total: 0, passed: 0 };
1128
+ execByFramework[f].total++;
1129
+ if (e.passed) execByFramework[f].passed++;
1130
+ });
1131
+ const projectName = path5.basename(resolved);
1132
+ reports.push({
1133
+ project: projectName,
1134
+ path: resolved,
1135
+ summary: {
1136
+ eventsTotal: events.length,
1137
+ eventTypes: byEventType,
1138
+ testRuns: { total: testRuns.length, passed: testRunPassed, failed: testRunFailed },
1139
+ byFramework,
1140
+ executions: { total: executions.length, byFramework: execByFramework },
1141
+ learnings: { total: learnings.length, byType: byLearningType },
1142
+ lastUpdated: metrics.lastUpdated || memory.updatedAt
1143
+ },
1144
+ recentEvents: events.slice(-20).map((e) => ({
1145
+ type: e.type,
1146
+ timestamp: e.timestamp,
1147
+ framework: e.framework,
1148
+ spec: e.spec,
1149
+ passed: e.passed,
1150
+ failed: e.failed,
1151
+ exitCode: e.exitCode,
1152
+ durationSeconds: e.durationSeconds
1153
+ }))
1154
+ });
1155
+ }
1156
+ if (jsonOnly) {
1157
+ const out = JSON.stringify({ projects: reports, generatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
1158
+ if (outputFile) fs5.writeFileSync(outputFile, out, "utf8");
1159
+ else console.log(out);
1160
+ return;
1161
+ }
1162
+ let report = `# Relat\xF3rio de M\xE9tricas \u2014 mcp-lab-agent
1163
+
1164
+ `;
1165
+ report += `Gerado em: ${(/* @__PURE__ */ new Date()).toISOString()}
1166
+ `;
1167
+ report += `Projetos: ${reports.length}
1168
+
1169
+ `;
1170
+ report += `---
1171
+
1172
+ `;
1173
+ for (const r of reports) {
1174
+ report += `## ${r.project}
1175
+
1176
+ `;
1177
+ report += `**Caminho:** \`${r.path}\`
1178
+
1179
+ `;
1180
+ const s = r.summary;
1181
+ report += `### Eventos (.qa-lab-metrics.json)
1182
+
1183
+ `;
1184
+ report += `| M\xE9todo/Tipo | Total | Descri\xE7\xE3o |
1185
+ `;
1186
+ report += `|-------------|-------|
1187
+ `;
1188
+ for (const [t, count] of Object.entries(s.eventTypes || {})) {
1189
+ let desc = "";
1190
+ if (t === "test_run") desc = `Execu\xE7\xE3o de testes (passed/failed por framework abaixo)`;
1191
+ else if (t === "bug_reported") desc = "Bug report gerado";
1192
+ else desc = t;
1193
+ report += `| ${t} | ${count} | ${desc} |
1194
+ `;
1195
+ }
1196
+ if (Object.keys(s.eventTypes || {}).length === 0) {
1197
+ report += `| \u2014 | 0 | Nenhum evento registrado |
1198
+ `;
1199
+ }
1200
+ report += `
1201
+ `;
1202
+ if (s.testRuns?.total > 0) {
1203
+ report += `### Resultado de Execu\xE7\xF5es (run_tests)
1204
+
1205
+ `;
1206
+ report += `| Framework | Total | Passed | Failed | Taxa sucesso |
1207
+ `;
1208
+ report += `|-----------|-------|--------|--------|---------------|
1209
+ `;
1210
+ for (const [fw, data] of Object.entries(s.byFramework || {})) {
1211
+ const rate = data.total > 0 ? Math.round(data.passed / data.total * 100) : 0;
1212
+ report += `| ${fw} | ${data.total} | ${data.passed} | ${data.failed} | ${rate}% |
1213
+ `;
1214
+ }
1215
+ report += `
1216
+ `;
1217
+ report += `**Resumo:** ${s.testRuns.passed} passed, ${s.testRuns.failed} failed (total: ${s.testRuns.total})
1218
+
1219
+ `;
1220
+ }
1221
+ if (s.executions?.total > 0) {
1222
+ report += `### Hist\xF3rico de Execu\xE7\xF5es (memory)
1223
+
1224
+ `;
1225
+ report += `| Framework | Total | Passed | Taxa |
1226
+ `;
1227
+ report += `|-----------|-------|--------|------|
1228
+ `;
1229
+ for (const [fw, data] of Object.entries(s.executions.byFramework || {})) {
1230
+ const rate = data.total > 0 ? Math.round(data.passed / data.total * 100) : 0;
1231
+ report += `| ${fw} | ${data.total} | ${data.passed} | ${rate}% |
1232
+ `;
1233
+ }
1234
+ report += `
1235
+ `;
1236
+ }
1237
+ if (s.learnings?.total > 0) {
1238
+ report += `### Aprendizados (.qa-lab-memory.json)
1239
+
1240
+ `;
1241
+ report += `| Tipo | Total | Sucesso | Taxa |
1242
+ `;
1243
+ report += `|------|-------|---------|------|
1244
+ `;
1245
+ for (const [t, data] of Object.entries(s.learnings.byType || {})) {
1246
+ const rate = data.total > 0 ? Math.round(data.success / data.total * 100) : 0;
1247
+ report += `| ${t} | ${data.total} | ${data.success} | ${rate}% |
1248
+ `;
1249
+ }
1250
+ report += `
1251
+ `;
1252
+ }
1253
+ if (r.recentEvents?.length > 0) {
1254
+ report += `### \xDAltimos 20 eventos
1255
+
1256
+ `;
1257
+ report += `| Data | Tipo | Framework | Spec | Passed | Failed | Exit | Dura\xE7\xE3o(s) |
1258
+ `;
1259
+ report += `|------|------|-----------|------|--------|--------|------|------------|
1260
+ `;
1261
+ for (const e of r.recentEvents.slice(-10)) {
1262
+ const ts = e.timestamp ? new Date(e.timestamp).toLocaleString() : "\u2014";
1263
+ report += `| ${ts} | ${e.type || "\u2014"} | ${e.framework || "\u2014"} | ${(e.spec || "\u2014").slice(0, 20)} | ${e.passed ?? "\u2014"} | ${e.failed ?? "\u2014"} | ${e.exitCode ?? "\u2014"} | ${e.durationSeconds ?? "\u2014"} |
1264
+ `;
1265
+ }
1266
+ report += `
1267
+ `;
1268
+ }
1269
+ if (s.lastUpdated) {
1270
+ report += `*\xDAltima atualiza\xE7\xE3o: ${s.lastUpdated}*
1271
+
1272
+ `;
1273
+ }
1274
+ report += `---
1275
+
1276
+ `;
1277
+ }
1278
+ if (outputFile) {
1279
+ fs5.writeFileSync(outputFile, report, "utf8");
1280
+ console.log(`
1281
+ \u{1F4C4} Relat\xF3rio salvo em: ${outputFile}
1282
+ `);
1283
+ } else {
1284
+ console.log(report);
1285
+ }
1286
+ }
1287
+ async function handleFlakyReportCommand() {
1288
+ const argv = process.argv.slice(2);
1289
+ const runsIdx = argv.indexOf("--runs");
1290
+ const runs = runsIdx !== -1 && argv[runsIdx + 1] ? parseInt(argv[runsIdx + 1], 10) : 3;
1291
+ const specIdx = argv.indexOf("--spec");
1292
+ const spec = specIdx !== -1 && argv[specIdx + 1] ? argv[specIdx + 1] : null;
1293
+ const outputIdx = argv.indexOf("--output");
1294
+ const outputFile = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : null;
1295
+ const structure = detectProjectStructure();
1296
+ if (!structure.hasTests) {
1297
+ console.error("\u274C Nenhum framework de teste detectado.");
1298
+ process.exit(1);
1299
+ }
1300
+ const fw = structure.testFrameworks[0];
1301
+ const { cmd, args, cwd } = getRunCommand(structure, fw, spec);
1302
+ console.log(`
1303
+ \u{1F52C} Relat\xF3rio de testes flaky
1304
+ `);
1305
+ console.log(`Framework: ${fw}`);
1306
+ console.log(`Execu\xE7\xF5es: ${runs}`);
1307
+ if (spec) console.log(`Spec: ${spec}`);
1308
+ console.log(`
1309
+ Rodando testes ${runs}x...
1310
+ `);
1311
+ const results = [];
1312
+ for (let i = 0; i < runs; i++) {
1313
+ process.stdout.write(` [${i + 1}/${runs}] `);
1314
+ const result = await runTestsOnce(cmd, args, cwd);
1315
+ results.push(result);
1316
+ process.stdout.write(result.passed ? "\u2705 passou\n" : "\u274C falhou\n");
1317
+ }
1318
+ const passed = results.filter((r) => r.passed).length;
1319
+ const failed = results.filter((r) => !r.passed).length;
1320
+ const isFlaky = passed > 0 && failed > 0;
1321
+ const failureOutput = results.find((r) => !r.passed)?.output || "";
1322
+ const flakyAnalysis = failureOutput ? detectFlakyPatterns(failureOutput) : null;
1323
+ const probableCause = flakyAnalysis?.isLikelyFlaky ? flakyAnalysis.patterns.map((p) => `${p.pattern}: ${p.suggestion}`).join("; ") : "N\xE3o foi poss\xEDvel inferir (rode com explainOnFailure para an\xE1lise detalhada)";
1324
+ let report = `# Relat\xF3rio de testes flaky \u2014 mcp-lab-agent
1325
+
1326
+ `;
1327
+ report += `Gerado em: ${(/* @__PURE__ */ new Date()).toISOString()}
1328
+ `;
1329
+ report += `Framework: ${fw}
1330
+ `;
1331
+ report += `Execu\xE7\xF5es: ${runs}
1332
+ `;
1333
+ if (spec) report += `Spec: ${spec}
1334
+ `;
1335
+ report += `
1336
+ ---
1337
+
1338
+ `;
1339
+ report += `## Resultado
1340
+
1341
+ `;
1342
+ report += `| M\xE9trica | Valor |
1343
+ `;
1344
+ report += `|---------|-------|
1345
+ `;
1346
+ report += `| Passou | ${passed}/${runs} |
1347
+ `;
1348
+ report += `| Falhou | ${failed}/${runs} |
1349
+ `;
1350
+ report += `| Taxa de falha | ${Math.round(failed / runs * 100)}% |
1351
+ `;
1352
+ report += `| **Flaky?** | ${isFlaky ? "\u26A0\uFE0F SIM" : failed === runs ? "\u274C Falha consistente" : "\u2705 Est\xE1vel"} |
1353
+
1354
+ `;
1355
+ if (isFlaky) {
1356
+ report += `## Causa prov\xE1vel
1357
+
1358
+ `;
1359
+ report += `${probableCause}
1360
+
1361
+ `;
1362
+ if (flakyAnalysis?.patterns?.length) {
1363
+ report += `### Sugest\xF5es
1364
+
1365
+ `;
1366
+ flakyAnalysis.patterns.forEach((p) => {
1367
+ report += `- **${p.pattern}:** ${p.suggestion}
1368
+ `;
1369
+ });
1370
+ report += `
1371
+ `;
1372
+ }
1373
+ }
1374
+ if (failed > 0 && failureOutput) {
1375
+ report += `## \xDAltima sa\xEDda de falha (trecho)
1376
+
1377
+ `;
1378
+ report += "```\n";
1379
+ report += failureOutput.slice(0, 1500).trim();
1380
+ if (failureOutput.length > 1500) report += "\n...";
1381
+ report += "\n```\n\n";
1382
+ }
1383
+ report += `---
1384
+
1385
+ `;
1386
+ report += `*Use \`mcp-lab-agent por_que_falhou\` (via MCP) ou \`run_tests\` com \`explainOnFailure: true\` para an\xE1lise detalhada.*
1387
+ `;
1388
+ if (outputFile) {
1389
+ fs5.writeFileSync(outputFile, report, "utf8");
1390
+ console.log(`
1391
+ \u{1F4C4} Relat\xF3rio salvo em: ${outputFile}
1392
+ `);
1393
+ } else {
1394
+ console.log("\n" + report);
1395
+ }
1396
+ process.exit(isFlaky ? 1 : 0);
1397
+ }
1398
+ function getRunCommand(structure, fw, spec) {
1399
+ const cwdMap = {
1400
+ cypress: structure.testDirs.includes("cypress") ? path5.join(PROJECT_ROOT5, "cypress") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5,
1401
+ playwright: structure.testDirs.includes("playwright") ? path5.join(PROJECT_ROOT5, "playwright") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5
1402
+ };
1403
+ const cwd = cwdMap[fw] || getFrameworkCwd(structure, ["specs", "tests", "e2e"]) || PROJECT_ROOT5;
1404
+ if (fw === "cypress") {
1405
+ return { cmd: "npx", args: spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"], cwd };
1406
+ }
1407
+ if (fw === "playwright") {
1408
+ return { cmd: "npx", args: spec ? ["playwright", "test", spec] : ["playwright", "test"], cwd };
1409
+ }
1410
+ if (fw === "webdriverio" || fw === "appium") {
1411
+ return { cmd: "npx", args: spec ? ["wdio", "run", spec] : ["wdio", "run"], cwd: PROJECT_ROOT5 };
1412
+ }
1413
+ if (fw === "jest") {
1414
+ return { cmd: "npx", args: spec ? ["jest", spec] : ["jest"], cwd: PROJECT_ROOT5 };
1415
+ }
1416
+ if (fw === "vitest") {
1417
+ return { cmd: "npx", args: ["vitest", "run", ...spec ? [spec] : []], cwd: PROJECT_ROOT5 };
1418
+ }
1419
+ if (fw === "mocha") {
1420
+ return { cmd: "npx", args: spec ? ["mocha", spec] : ["mocha"], cwd: PROJECT_ROOT5 };
1421
+ }
1422
+ if (fw === "pytest") {
1423
+ return { cmd: "pytest", args: spec ? [spec] : [], cwd: PROJECT_ROOT5 };
1424
+ }
1425
+ if (fw === "robot") {
1426
+ return { cmd: "robot", args: spec ? [spec] : [structure.testDirs[0] || "tests"], cwd: PROJECT_ROOT5 };
1427
+ }
1428
+ return { cmd: "npm", args: ["test"], cwd: PROJECT_ROOT5 };
1429
+ }
1430
+ async function handleRunCommand() {
1431
+ const argv = process.argv.slice(2);
1432
+ const deviceIdx = argv.indexOf("--device");
1433
+ const device = deviceIdx !== -1 && argv[deviceIdx + 1] ? argv[deviceIdx + 1] : null;
1434
+ const noAutoFix = argv.includes("--no-auto-fix");
1435
+ const spec = argv.filter((a) => !a.startsWith("--") && a !== "run")[0] || null;
1436
+ const structure = detectProjectStructure();
1437
+ if (!structure.hasTests) {
1438
+ console.error("\u274C Nenhum framework de teste detectado.");
1439
+ process.exit(1);
1440
+ }
1441
+ const fw = structure.testFrameworks[0];
1442
+ const deviceConfig = structure.hasMobile ? detectDeviceConfig(structure) : {};
1443
+ const useDevice = device || deviceConfig.configuration || deviceConfig.device;
1444
+ const doAutoFix = !noAutoFix && structure.hasMobile && !!spec;
1445
+ let runEnv = { ...process.env };
1446
+ if (Object.keys(deviceConfig.envOverrides || {}).length) {
1447
+ runEnv = { ...runEnv, ...deviceConfig.envOverrides };
681
1448
  }
682
- if (cmd === "stats") {
683
- const stats = getMemoryStats();
684
- console.log(`
685
- \u{1F4CA} Estat\xEDsticas de Aprendizado
686
-
687
- Total de aprendizados: ${stats.totalLearnings}
688
- Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
689
- Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
690
- Corre\xE7\xF5es de timing: ${stats.timingFixes}
691
- Testes gerados: ${stats.testsGenerated}
692
- Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
693
-
694
- ${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." : ""}
1449
+ if (device) {
1450
+ if (fw === "detox") runEnv.DETOX_CONFIGURATION = device;
1451
+ else if (fw === "appium") runEnv.APPIUM_DEVICE_NAME = device;
1452
+ } else if (deviceConfig.configuration && fw === "detox") {
1453
+ runEnv.DETOX_CONFIGURATION = deviceConfig.configuration;
1454
+ }
1455
+ let { cmd, args, cwd } = getRunCommand(structure, fw, spec);
1456
+ if (fw === "detox" && useDevice) {
1457
+ args = [...args.slice(0, 2), "--configuration", useDevice, ...args.slice(2)];
1458
+ }
1459
+ const isSelectorFailure = (out) => /element not found|selector|timeout|locator|cy\.get|page\.locator|Unable to find/i.test(out || "");
1460
+ console.log(`
1461
+ \u25B6\uFE0F Rodando testes${spec ? `: ${spec}` : ""}
695
1462
  `);
696
- return true;
1463
+ if (useDevice) console.log(` Device: ${useDevice}
1464
+ `);
1465
+ let result = await runTestsOnce(cmd, args, cwd, runEnv);
1466
+ let autoFixed = false;
1467
+ if (!result.passed && doAutoFix && isSelectorFailure(result.runOutput) && resolveLLMProvider("complex").apiKey) {
1468
+ console.log("\n\u26A0\uFE0F Falha por seletor. Aplicando corre\xE7\xE3o autom\xE1tica...\n");
1469
+ const fixResult = await applySelectorFixAndRetry(spec, result.runOutput, fw);
1470
+ if (fixResult.applied) {
1471
+ autoFixed = true;
1472
+ result = await runTestsOnce(cmd, args, cwd, runEnv);
1473
+ }
697
1474
  }
698
- if (cmd === "analyze") {
699
- await handleAnalyzeCommand();
700
- return true;
1475
+ if (result.passed) {
1476
+ console.log(`
1477
+ \u2705 Testes passaram${autoFixed ? " (ap\xF3s corre\xE7\xE3o de seletor)" : ""}.
1478
+ `);
1479
+ } else {
1480
+ console.log(`
1481
+ \u274C Testes falharam.
1482
+ `);
1483
+ if (result.runOutput) console.log(result.runOutput.slice(0, 800) + (result.runOutput.length > 800 ? "\n..." : ""));
701
1484
  }
702
- return false;
1485
+ process.exit(result.passed ? 0 : 1);
1486
+ }
1487
+ function runTestsOnce(cmd, args, cwd, env = process.env) {
1488
+ return new Promise((resolve) => {
1489
+ const child = spawn(cmd, args, {
1490
+ cwd,
1491
+ stdio: ["inherit", "pipe", "pipe"],
1492
+ shell: process.platform === "win32",
1493
+ env: { ...process.env, ...env }
1494
+ });
1495
+ let stdout = "";
1496
+ let stderr = "";
1497
+ if (child.stdout) child.stdout.on("data", (d) => {
1498
+ stdout += d.toString();
1499
+ });
1500
+ if (child.stderr) child.stderr.on("data", (d) => {
1501
+ stderr += d.toString();
1502
+ });
1503
+ child.on("close", (code) => {
1504
+ const output = [stdout, stderr].filter(Boolean).join("\n");
1505
+ resolve({ passed: code === 0, code: code ?? 1, output });
1506
+ });
1507
+ });
703
1508
  }
704
1509
  async function handleAutoCommand() {
705
1510
  const request = process.argv.slice(3).join(" ");
@@ -781,16 +1586,16 @@ Framework: ${fw}`;
781
1586
  const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
782
1587
  const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
783
1588
  const safeName = fileName + ext;
784
- testFilePath = path4.join(baseDir, safeName);
785
- if (!fs4.existsSync(baseDir)) fs4.mkdirSync(baseDir, { recursive: true });
1589
+ testFilePath = path5.join(baseDir, safeName);
1590
+ if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
786
1591
  }
787
- fs4.writeFileSync(testFilePath, testContent, "utf8");
1592
+ fs5.writeFileSync(testFilePath, testContent, "utf8");
788
1593
  console.log(`\u2705 Teste gravado: ${testFilePath}`);
789
1594
  console.log(`
790
1595
  [Tentativa ${attempt}/${maxRetries}] Executando teste...`);
791
1596
  const runResult = await new Promise((resolve) => {
792
1597
  const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
793
- cwd: PROJECT_ROOT4,
1598
+ cwd: PROJECT_ROOT5,
794
1599
  stdio: ["inherit", "pipe", "pipe"],
795
1600
  shell: process.platform === "win32"
796
1601
  });
@@ -857,9 +1662,9 @@ async function handleAnalyzeCommand() {
857
1662
  console.log(`\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
858
1663
  `);
859
1664
  const testFiles = structure.testDirs.flatMap((dir) => {
860
- const fullPath = path4.join(PROJECT_ROOT4, dir);
861
- if (!fs4.existsSync(fullPath)) return [];
862
- return fs4.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
1665
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
1666
+ if (!fs5.existsSync(fullPath)) return [];
1667
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
863
1668
  });
864
1669
  console.log(`\u2705 ${testFiles.length} teste(s) encontrado(s)
865
1670
  `);
@@ -918,13 +1723,13 @@ async function handleAnalyzeCommand() {
918
1723
  }
919
1724
 
920
1725
  // src/index.js
921
- var PROJECT_ROOT5 = process.cwd();
922
- config({ path: path5.join(PROJECT_ROOT5, ".env") });
1726
+ var PROJECT_ROOT6 = process.cwd();
1727
+ config({ path: path6.join(PROJECT_ROOT6, ".env") });
923
1728
  var server = new McpServer({
924
1729
  name: "mcp-lab-agent",
925
- version: "2.1.0"
1730
+ version: "2.1.9"
926
1731
  });
927
- var METRICS_FILE2 = path5.join(PROJECT_ROOT5, ".qa-lab-metrics.json");
1732
+ var METRICS_FILE2 = path6.join(PROJECT_ROOT6, ".qa-lab-metrics.json");
928
1733
  function appendMetricsEvent(event) {
929
1734
  recordMetricEvent(event);
930
1735
  }
@@ -945,20 +1750,20 @@ server.registerTool(
945
1750
  },
946
1751
  async ({ path: filePath, encoding = "utf8" }) => {
947
1752
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
948
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
949
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
1753
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
1754
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
950
1755
  return {
951
1756
  content: [{ type: "text", text: "Caminho fora do projeto." }],
952
1757
  structuredContent: { ok: false, error: "Path outside project" }
953
1758
  };
954
1759
  }
955
- if (!fs5.existsSync(fullPath)) {
1760
+ if (!fs6.existsSync(fullPath)) {
956
1761
  return {
957
1762
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
958
1763
  structuredContent: { ok: false, error: "File not found" }
959
1764
  };
960
1765
  }
961
- const stat = fs5.statSync(fullPath);
1766
+ const stat = fs6.statSync(fullPath);
962
1767
  if (stat.isDirectory()) {
963
1768
  return {
964
1769
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Use um caminho de arquivo." }],
@@ -966,7 +1771,7 @@ server.registerTool(
966
1771
  };
967
1772
  }
968
1773
  try {
969
- const content = fs5.readFileSync(fullPath, encoding);
1774
+ const content = fs6.readFileSync(fullPath, encoding);
970
1775
  return {
971
1776
  content: [{ type: "text", text: content }],
972
1777
  structuredContent: { ok: true, content }
@@ -983,7 +1788,7 @@ server.registerTool(
983
1788
  "detect_project",
984
1789
  {
985
1790
  title: "Detectar estrutura do projeto",
986
- description: "Analisa o projeto e identifica frameworks de teste, pastas, backend, frontend.",
1791
+ description: "Analisa o projeto e identifica frameworks de teste, pastas, backend, frontend, ambiente (web/mobile) e hints para gera\xE7\xE3o de testes.",
987
1792
  inputSchema: z.object({}),
988
1793
  outputSchema: z.object({
989
1794
  ok: z.boolean(),
@@ -994,17 +1799,22 @@ server.registerTool(
994
1799
  hasBackend: z.boolean(),
995
1800
  backendDir: z.string().nullable(),
996
1801
  hasFrontend: z.boolean(),
997
- frontendDir: z.string().nullable()
1802
+ frontendDir: z.string().nullable(),
1803
+ hasMobile: z.boolean().optional(),
1804
+ environment: z.string().optional(),
1805
+ environmentHints: z.array(z.string()).optional()
998
1806
  })
999
1807
  })
1000
1808
  },
1001
1809
  async () => {
1002
1810
  const structure = detectProjectStructure();
1811
+ const envLine = structure.environment ? `Ambiente: ${structure.environment}${structure.environmentHints?.length ? ` (${structure.environmentHints.join(", ")})` : ""}` : "";
1003
1812
  const summary = [
1004
1813
  `Frameworks de teste: ${structure.testFrameworks.join(", ") || "nenhum"}`,
1005
1814
  `Pastas de teste: ${structure.testDirs.join(", ") || "nenhuma"}`,
1006
1815
  `Backend: ${structure.backendDir || "n\xE3o detectado"}`,
1007
- `Frontend: ${structure.frontendDir || "n\xE3o detectado"}`
1816
+ `Frontend: ${structure.frontendDir || "n\xE3o detectado"}`,
1817
+ ...envLine ? [envLine] : []
1008
1818
  ].join("\n");
1009
1819
  return {
1010
1820
  content: [{ type: "text", text: summary }],
@@ -1045,7 +1855,7 @@ server.registerTool(
1045
1855
  structuredContent: { ok: false, error: "Playwright not installed. Run: npm install playwright" }
1046
1856
  };
1047
1857
  }
1048
- const outPath = screenshotPath ? path5.join(PROJECT_ROOT5, screenshotPath.replace(/^\//, "")) : path5.join(PROJECT_ROOT5, ".qa-lab-screenshot.png");
1858
+ const outPath = screenshotPath ? path6.join(PROJECT_ROOT6, screenshotPath.replace(/^\//, "")) : path6.join(PROJECT_ROOT6, ".qa-lab-screenshot.png");
1049
1859
  const consoleLogs = [];
1050
1860
  const consoleErrors = [];
1051
1861
  const networkRequests = [];
@@ -1072,7 +1882,7 @@ server.registerTool(
1072
1882
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
1073
1883
  await page.screenshot({ path: outPath, fullPage: false });
1074
1884
  await browser.close();
1075
- const relPath = path5.relative(PROJECT_ROOT5, outPath);
1885
+ const relPath = path6.relative(PROJECT_ROOT6, outPath);
1076
1886
  let summary = `Screenshot salvo: ${relPath}`;
1077
1887
  if (consoleErrors.length) summary += `
1078
1888
 
@@ -1104,11 +1914,11 @@ var QA_AGENTS2 = {
1104
1914
  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" },
1105
1915
  detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
1106
1916
  execution: { tools: ["run_tests", "watch_tests", "get_test_coverage"], desc: "Execu\xE7\xE3o de testes e cobertura" },
1107
- generation: { tools: ["generate_tests", "write_test", "create_test_template"], desc: "Gera\xE7\xE3o de testes com LLM" },
1917
+ generation: { tools: ["generate_tests", "write_test", "create_test_template", "map_mobile_elements"], desc: "Gera\xE7\xE3o de testes com LLM" },
1108
1918
  analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
1109
1919
  browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
1110
1920
  reporting: { tools: ["create_bug_report", "get_business_metrics"], desc: "Relat\xF3rios e m\xE9tricas" },
1111
- learning: { tools: ["qa_learning_stats", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
1921
+ learning: { tools: ["qa_learning_stats", "get_learning_report", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
1112
1922
  maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
1113
1923
  };
1114
1924
  server.registerTool(
@@ -1140,8 +1950,17 @@ server.registerTool(
1140
1950
  if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
1141
1951
  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 } };
1142
1952
  }
1953
+ if (/mapear|elementos mobile|deep link|deeplink|app package|bundle.?id|appium inspector/i.test(t)) {
1954
+ return { content: [{ type: "text", text: "Agente: generation \u2192 map_mobile_elements (mapear elementos), depois generate_tests + write_test" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: ["map_mobile_elements", "generate_tests", "write_test"], description: QA_AGENTS2.generation.desc } };
1955
+ }
1956
+ if (/mapear|elementos mobile|deep link|deeplink|app package|bundle.?id/i.test(t)) {
1957
+ return { content: [{ type: "text", text: "Agente: generation \u2192 map_mobile_elements (mapear elementos), depois generate_tests + write_test" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: ["map_mobile_elements", "generate_tests", "write_test"], description: "Mapeamento de elementos mobile + gera\xE7\xE3o de testes" } };
1958
+ }
1959
+ if (/mobile|deeplink|deep link|elementos|mapear.*app|appium|detox/i.test(t) && !/rodar|run|executar/i.test(t)) {
1960
+ return { content: [{ type: "text", text: "Agente: generation \u2192 map_mobile_elements, generate_tests, write_test (mobile)" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: QA_AGENTS2.generation.tools, description: QA_AGENTS2.generation.desc } };
1961
+ }
1143
1962
  if (/gerar|criar|escrever|generate|write|template/i.test(t)) {
1144
- 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 } };
1963
+ return { content: [{ type: "text", text: "Agente: generation \u2192 generate_tests, write_test, map_mobile_elements" }], structuredContent: { ok: true, suggestedAgent: "generation", suggestedTools: QA_AGENTS2.generation.tools, description: QA_AGENTS2.generation.desc } };
1145
1964
  }
1146
1965
  if (/analisar|por que|falhou|suggest|correção|selector|fix/i.test(t)) {
1147
1966
  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 } };
@@ -1185,7 +2004,9 @@ server.registerTool(
1185
2004
  ]).optional().describe("Framework espec\xEDfico ou 'npm' para npm test."),
1186
2005
  spec: z.string().optional().describe("Caminho do spec (ex: cypress/e2e/test.cy.js)."),
1187
2006
  suite: z.string().optional().describe("Suite ou pattern (ex: e2e, api)."),
1188
- explainOnFailure: z.boolean().optional().describe("Se true, quando falhar gera automaticamente: O que aconteceu, Por que falhou, O que fazer, Sugest\xE3o de corre\xE7\xE3o. Requer API key.")
2007
+ device: z.string().optional().describe("Device/configuration para mobile. Se vazio, detecta de qa-lab-agent.config.json, wdio.conf ou .detoxrc."),
2008
+ explainOnFailure: z.boolean().optional().describe("Se true, quando falhar gera automaticamente: O que aconteceu, Por que falhou, O que fazer, Sugest\xE3o de corre\xE7\xE3o. Requer API key."),
2009
+ autoFixSelector: z.boolean().optional().describe("Se true e falhar por seletor, aplica corre\xE7\xE3o automaticamente e tenta novamente. Requer spec e API key. Default: true para mobile.")
1189
2010
  }),
1190
2011
  outputSchema: z.object({
1191
2012
  status: z.enum(["passed", "failed", "not_found"]),
@@ -1194,7 +2015,7 @@ server.registerTool(
1194
2015
  runOutput: z.string().optional()
1195
2016
  })
1196
2017
  },
1197
- async ({ framework, spec, suite, explainOnFailure }) => {
2018
+ async ({ framework, spec, suite, explainOnFailure, device, autoFixSelector }) => {
1198
2019
  const structure = detectProjectStructure();
1199
2020
  if (!structure.hasTests) {
1200
2021
  return {
@@ -1210,15 +2031,28 @@ server.registerTool(
1210
2031
  if (!selectedFramework && structure.testFrameworks.length > 0) {
1211
2032
  selectedFramework = structure.testFrameworks[0];
1212
2033
  }
2034
+ const deviceConfig = structure.hasMobile ? detectDeviceConfig(structure) : {};
2035
+ const useDevice = device || deviceConfig.configuration || deviceConfig.device;
2036
+ const doAutoFixSelector = autoFixSelector ?? (structure.hasMobile && !!spec);
2037
+ let runEnv = { ...process.env };
2038
+ if (useDevice && Object.keys(deviceConfig.envOverrides || {}).length) {
2039
+ runEnv = { ...runEnv, ...deviceConfig.envOverrides };
2040
+ }
2041
+ if (device) {
2042
+ if (selectedFramework === "detox") runEnv.DETOX_CONFIGURATION = device;
2043
+ else if (selectedFramework === "appium") runEnv.APPIUM_DEVICE_NAME = device;
2044
+ } else if (deviceConfig.configuration && selectedFramework === "detox") {
2045
+ runEnv.DETOX_CONFIGURATION = deviceConfig.configuration;
2046
+ }
1213
2047
  let cmd, args, cwd;
1214
2048
  if (selectedFramework === "cypress") {
1215
2049
  cmd = "npx";
1216
2050
  args = spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"];
1217
- cwd = structure.testDirs.includes("cypress") ? path5.join(PROJECT_ROOT5, "cypress") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5;
2051
+ cwd = structure.testDirs.includes("cypress") ? path6.join(PROJECT_ROOT6, "cypress") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
1218
2052
  } else if (selectedFramework === "playwright") {
1219
2053
  cmd = "npx";
1220
2054
  args = spec ? ["playwright", "test", spec] : ["playwright", "test"];
1221
- cwd = structure.testDirs.includes("playwright") ? path5.join(PROJECT_ROOT5, "playwright") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5;
2055
+ cwd = structure.testDirs.includes("playwright") ? path6.join(PROJECT_ROOT6, "playwright") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
1222
2056
  } else if (selectedFramework === "webdriverio") {
1223
2057
  cmd = "npx";
1224
2058
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
@@ -1243,108 +2077,138 @@ server.registerTool(
1243
2077
  cmd = "npx";
1244
2078
  args = ["jest"];
1245
2079
  if (spec) args.push(spec);
1246
- cwd = PROJECT_ROOT5;
2080
+ cwd = PROJECT_ROOT6;
1247
2081
  } else if (selectedFramework === "vitest") {
1248
2082
  cmd = "npx";
1249
2083
  args = ["vitest", "run"];
1250
2084
  if (spec) args.push(spec);
1251
- cwd = PROJECT_ROOT5;
2085
+ cwd = PROJECT_ROOT6;
1252
2086
  } else if (selectedFramework === "mocha") {
1253
2087
  cmd = "npx";
1254
2088
  args = spec ? ["mocha", spec] : ["mocha"];
1255
- cwd = PROJECT_ROOT5;
2089
+ cwd = PROJECT_ROOT6;
1256
2090
  } else if (selectedFramework === "appium") {
1257
2091
  cmd = "npx";
1258
2092
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
1259
- cwd = PROJECT_ROOT5;
2093
+ cwd = PROJECT_ROOT6;
1260
2094
  } else if (selectedFramework === "detox") {
1261
2095
  cmd = "npx";
1262
2096
  args = ["detox", "test"];
2097
+ if (useDevice) args.push("--configuration", useDevice);
1263
2098
  if (spec) args.push(spec);
1264
- cwd = PROJECT_ROOT5;
2099
+ cwd = PROJECT_ROOT6;
1265
2100
  } else if (selectedFramework === "robot") {
1266
2101
  cmd = "robot";
1267
2102
  args = spec ? [spec] : [structure.testDirs[0] || "tests"];
1268
- cwd = PROJECT_ROOT5;
2103
+ cwd = PROJECT_ROOT6;
1269
2104
  } else if (selectedFramework === "pytest") {
1270
2105
  cmd = "pytest";
1271
2106
  args = spec ? [spec] : [];
1272
- cwd = PROJECT_ROOT5;
2107
+ cwd = PROJECT_ROOT6;
1273
2108
  } else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
1274
2109
  cmd = "npm";
1275
2110
  args = ["test"];
1276
- cwd = PROJECT_ROOT5;
2111
+ cwd = PROJECT_ROOT6;
1277
2112
  } else {
1278
2113
  cmd = "npm";
1279
2114
  args = ["test"];
1280
- cwd = PROJECT_ROOT5;
2115
+ cwd = PROJECT_ROOT6;
1281
2116
  }
1282
- const startTime = Date.now();
1283
- return new Promise((resolve) => {
2117
+ const runTestsOnce2 = () => new Promise((resolve) => {
2118
+ const startTime = Date.now();
1284
2119
  const child = spawn2(cmd, args, {
1285
2120
  cwd,
1286
2121
  stdio: ["inherit", "pipe", "pipe"],
1287
2122
  shell: process.platform === "win32",
1288
- env: { ...process.env }
2123
+ env: runEnv
1289
2124
  });
1290
2125
  let stdout = "";
1291
2126
  let stderr = "";
1292
- if (child.stdout) {
1293
- child.stdout.on("data", (d) => {
1294
- const s = d.toString();
1295
- stdout += s;
1296
- process.stdout.write(s);
1297
- });
1298
- }
1299
- if (child.stderr) {
1300
- child.stderr.on("data", (d) => {
1301
- const s = d.toString();
1302
- stderr += s;
1303
- process.stderr.write(s);
1304
- });
1305
- }
2127
+ if (child.stdout) child.stdout.on("data", (d) => {
2128
+ stdout += d.toString();
2129
+ process.stdout.write(d);
2130
+ });
2131
+ if (child.stderr) child.stderr.on("data", (d) => {
2132
+ stderr += d.toString();
2133
+ process.stderr.write(d);
2134
+ });
1306
2135
  child.on("close", (code) => {
1307
2136
  const runOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
1308
- const passed = code === 0;
1309
- const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
1310
- if (!passed && runOutput) {
1311
- try {
1312
- fs5.writeFileSync(path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log"), runOutput, "utf8");
1313
- } catch {
1314
- }
1315
- }
1316
- const { passed: p, failed: f } = parseTestRunResult(runOutput, code);
1317
- appendMetricsEvent({
1318
- type: "test_run",
1319
- framework: selectedFramework,
1320
- spec: spec || void 0,
1321
- passed: p,
1322
- failed: f,
1323
- durationSeconds,
1324
- exitCode: code ?? 1,
1325
- failures: !passed ? extractFailuresFromOutput(runOutput) : void 0
1326
- });
1327
- if (passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
1328
- saveProjectMemory({
1329
- execution: {
1330
- testFile: spec || "all",
1331
- passed,
1332
- duration: durationSeconds,
1333
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1334
- framework: selectedFramework
1335
- }
1336
- });
1337
2137
  resolve({
1338
- content: [{ type: "text", text: passed ? "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes." }],
1339
- structuredContent: {
1340
- status: passed ? "passed" : "failed",
1341
- message: passed ? "Tests passed" : "Tests failed",
1342
- exitCode: code ?? 1,
1343
- runOutput: !passed ? runOutput : void 0
1344
- }
2138
+ passed: code === 0,
2139
+ exitCode: code ?? 1,
2140
+ runOutput,
2141
+ durationSeconds: Math.round((Date.now() - startTime) / 1e3)
1345
2142
  });
1346
2143
  });
1347
2144
  });
2145
+ const isSelectorFailure = (out) => /element not found|selector|timeout|locator|cy\.get|page\.locator|Unable to find/i.test(out || "");
2146
+ let result = await runTestsOnce2();
2147
+ let autoFixed = false;
2148
+ if (!result.passed && doAutoFixSelector && spec && isSelectorFailure(result.runOutput) && resolveLLMProvider("complex").apiKey) {
2149
+ const fixResult = await applySelectorFixAndRetry(spec, result.runOutput, selectedFramework);
2150
+ if (fixResult.applied) {
2151
+ autoFixed = true;
2152
+ result = await runTestsOnce2();
2153
+ }
2154
+ }
2155
+ if (!result.passed && result.runOutput) {
2156
+ try {
2157
+ fs6.writeFileSync(path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log"), result.runOutput, "utf8");
2158
+ } catch {
2159
+ }
2160
+ }
2161
+ const { passed: p, failed: f } = parseTestRunResult(result.runOutput, result.exitCode);
2162
+ appendMetricsEvent({
2163
+ type: "test_run",
2164
+ framework: selectedFramework,
2165
+ spec: spec || void 0,
2166
+ passed: p,
2167
+ failed: f,
2168
+ durationSeconds: result.durationSeconds,
2169
+ exitCode: result.exitCode,
2170
+ failures: !result.passed ? extractFailuresFromOutput(result.runOutput) : void 0
2171
+ });
2172
+ if (result.passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
2173
+ saveProjectMemory({
2174
+ execution: {
2175
+ testFile: spec || "all",
2176
+ passed: result.passed,
2177
+ duration: result.durationSeconds,
2178
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2179
+ framework: selectedFramework
2180
+ }
2181
+ });
2182
+ const baseMsg = result.passed ? autoFixed ? "Testes executados com sucesso (ap\xF3s corre\xE7\xE3o autom\xE1tica de seletor)." : "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes.";
2183
+ const structured = {
2184
+ status: result.passed ? "passed" : "failed",
2185
+ message: result.passed ? "Tests passed" : "Tests failed",
2186
+ exitCode: result.exitCode,
2187
+ runOutput: !result.passed ? result.runOutput : void 0,
2188
+ autoFixed: autoFixed || void 0
2189
+ };
2190
+ if (!result.passed && explainOnFailure && result.runOutput) {
2191
+ const explainResult = await generateFailureExplanation(result.runOutput, spec || void 0);
2192
+ if (explainResult.ok && explainResult.structuredContent) {
2193
+ const oneLine = explainResult.structuredContent.resumoEmUmaFrase || oneLineFailureSummary(result.runOutput, selectedFramework, explainResult.structuredContent.oQueAconteceu, explainResult.structuredContent.sugestaoCorrecao);
2194
+ structured.explanation = explainResult.structuredContent.formattedText;
2195
+ structured.resumoEmUmaFrase = oneLine;
2196
+ return {
2197
+ content: [{ type: "text", text: `${baseMsg}
2198
+
2199
+ **${oneLine}**
2200
+
2201
+ ---
2202
+
2203
+ ${explainResult.structuredContent.formattedText}` }],
2204
+ structuredContent: structured
2205
+ };
2206
+ }
2207
+ }
2208
+ return {
2209
+ content: [{ type: "text", text: baseMsg }],
2210
+ structuredContent: structured
2211
+ };
1348
2212
  }
1349
2213
  );
1350
2214
  server.registerTool(
@@ -1433,10 +2297,10 @@ server.registerTool(
1433
2297
  ${referenceCode.slice(0, 8e3)}`;
1434
2298
  if (referencePaths?.length) {
1435
2299
  for (const p of referencePaths.slice(0, 5)) {
1436
- const full = path5.join(PROJECT_ROOT5, p.replace(/^\//, "").replace(/\\/g, "/"));
1437
- if (fs5.existsSync(full)) {
2300
+ const full = path6.join(PROJECT_ROOT6, p.replace(/^\//, "").replace(/\\/g, "/"));
2301
+ if (fs6.existsSync(full)) {
1438
2302
  try {
1439
- const content = fs5.readFileSync(full, "utf8");
2303
+ const content = fs6.readFileSync(full, "utf8");
1440
2304
  referenceBlock += `
1441
2305
 
1442
2306
  --- Arquivo: ${p} ---
@@ -1465,8 +2329,17 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
1465
2329
  - Mantenha a MESMA l\xF3gica e fluxo de teste
1466
2330
  - Traduza seletores, comandos e asser\xE7\xF5es para ${fw}
1467
2331
  - Use Page Objects se o projeto j\xE1 usa
1468
- - Retorne SOMENTE o c\xF3digo, sem markdown` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
2332
+ - Retorne SOMENTE o c\xF3digo, sem markdown
2333
+
2334
+ ${UNIVERSAL_TEST_PRACTICES}
2335
+ ${fw === "appium" || fw === "detox" ? `
2336
+ IMPORTANTE: ${MOBILE_MAPPING_LESSON}
2337
+
2338
+ HIERARQUIA DE SELETORES: ${MOBILE_SELECTOR_HIERARCHY}` : ""}` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
1469
2339
  Framework: ${fw}
2340
+
2341
+ ${UNIVERSAL_TEST_PRACTICES}
2342
+
1470
2343
  Regras:
1471
2344
  - Cypress: cy.request(), cy.visit(), cy.get()
1472
2345
  - Playwright: test(), test.describe(), page.goto(), page.locator()
@@ -1474,7 +2347,11 @@ Regras:
1474
2347
  - Jest/Vitest: describe(), test(), expect()
1475
2348
  - Robot: Keywords, [Tags], Steps
1476
2349
  - pytest: def test_*, assert, fixtures
1477
- - C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown`;
2350
+ - C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown${fw === "appium" || fw === "detox" ? `
2351
+
2352
+ IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}
2353
+
2354
+ HIERARQUIA: ${MOBILE_SELECTOR_HIERARCHY}` : ""}`;
1478
2355
  const userPrompt = `Contexto do projeto:
1479
2356
  ${contextWithMemory.slice(0, 5e3)}
1480
2357
 
@@ -1558,7 +2435,7 @@ function getExtensionAndBaseDir2(fw, structure) {
1558
2435
  robot: structure.testDirs.includes("robot") ? "robot" : structure.testDirs[0] || "tests",
1559
2436
  behave: structure.testDirs.includes("features") ? "features" : structure.testDirs[0] || "tests"
1560
2437
  };
1561
- const baseDir = path5.join(PROJECT_ROOT5, baseMap[fw] || structure.testDirs[0] || "tests");
2438
+ const baseDir = path6.join(PROJECT_ROOT6, baseMap[fw] || structure.testDirs[0] || "tests");
1562
2439
  return { ext, baseDir };
1563
2440
  }
1564
2441
  server.registerTool(
@@ -1603,13 +2480,13 @@ server.registerTool(
1603
2480
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
1604
2481
  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, "");
1605
2482
  const fileName = ext.startsWith("_") ? `${safeName}${ext}` : `${safeName}${ext}`;
1606
- const targetDir = subdir ? path5.join(baseDir, subdir) : baseDir;
1607
- const filePath = path5.join(targetDir, fileName);
2483
+ const targetDir = subdir ? path6.join(baseDir, subdir) : baseDir;
2484
+ const filePath = path6.join(targetDir, fileName);
1608
2485
  try {
1609
- if (!fs5.existsSync(targetDir)) {
1610
- fs5.mkdirSync(targetDir, { recursive: true });
2486
+ if (!fs6.existsSync(targetDir)) {
2487
+ fs6.mkdirSync(targetDir, { recursive: true });
1611
2488
  }
1612
- fs5.writeFileSync(filePath, content, "utf8");
2489
+ fs6.writeFileSync(filePath, content, "utf8");
1613
2490
  return {
1614
2491
  content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
1615
2492
  structuredContent: { ok: true, path: filePath }
@@ -1679,8 +2556,10 @@ server.registerTool(
1679
2556
  };
1680
2557
  }
1681
2558
  );
1682
- function formatFailureExplanation(data) {
1683
- const lines = [
2559
+ function formatFailureExplanation(data, oneLine = null) {
2560
+ const summary = oneLine || data.resumoEmUmaFrase || "";
2561
+ const lines = summary ? [`**${summary}**`, "", "---", ""] : [];
2562
+ lines.push(
1684
2563
  "## O que aconteceu",
1685
2564
  "",
1686
2565
  data.oQueAconteceu || "",
@@ -1692,7 +2571,7 @@ function formatFailureExplanation(data) {
1692
2571
  "## O que fazer agora",
1693
2572
  "",
1694
2573
  ...Array.isArray(data.oQueFazerAgora) ? data.oQueFazerAgora.map((s, i) => `${i + 1}. ${s}`) : [data.oQueFazerAgora || ""]
1695
- ];
2574
+ );
1696
2575
  if (data.sugestaoCorrecao) {
1697
2576
  lines.push("", "## Sugest\xE3o de corre\xE7\xE3o", "", "```" + (data.framework || "js"), data.sugestaoCorrecao, "```");
1698
2577
  }
@@ -1734,6 +2613,70 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
1734
2613
  const data = await res.json();
1735
2614
  return data.choices?.[0]?.message?.content || "";
1736
2615
  }
2616
+ async function generateFailureExplanation(resolvedOutput, testFilePath = null) {
2617
+ const structure = detectProjectStructure();
2618
+ const fw = structure.testFrameworks[0] || "unknown";
2619
+ let testCode = "";
2620
+ if (testFilePath) {
2621
+ const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
2622
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
2623
+ if (fs6.existsSync(fullPath) && !fs6.statSync(fullPath).isDirectory()) {
2624
+ try {
2625
+ testCode = fs6.readFileSync(fullPath, "utf8");
2626
+ } catch {
2627
+ }
2628
+ }
2629
+ }
2630
+ const llm = resolveLLMProvider("complex");
2631
+ if (!llm.apiKey) return { ok: false, structuredContent: null };
2632
+ const { provider, apiKey, baseUrl, model } = llm;
2633
+ const fwHints = {
2634
+ webdriverio: "WebdriverIO (describe/it, $, browser.$)",
2635
+ appium: "Appium/WebdriverIO (mobile, $, browser.$)",
2636
+ playwright: "Playwright (test, page, locator)",
2637
+ cypress: "Cypress (cy.get, cy.click)",
2638
+ jest: "Jest (describe, test, expect)",
2639
+ vitest: "Vitest (describe, test, expect)",
2640
+ robot: "Robot Framework",
2641
+ pytest: "pytest"
2642
+ };
2643
+ 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:
2644
+ - resumoEmUmaFrase: string (OBRIGAT\xD3RIO - uma frase: "Falhou porque X. Solu\xE7\xE3o: Y.")
2645
+ - oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
2646
+ - porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas)
2647
+ - oQueFazerAgora: array de strings (passos numerados do que fazer)
2648
+ - sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o no formato do framework)
2649
+ - conceito: string ou null
2650
+ - framework: string (framework do projeto)
2651
+
2652
+ Framework: ${fw}. ${fwHints[fw] || ""}
2653
+ Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
2654
+ const userPrompt = `Output do terminal/log (teste falhou):
2655
+ ---
2656
+ ${resolvedOutput.slice(0, 12e3)}
2657
+ ---
2658
+ ${testCode ? `
2659
+ C\xF3digo do teste:
2660
+ ---
2661
+ ${testCode.slice(0, 6e3)}
2662
+ ---` : ""}`;
2663
+ try {
2664
+ let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
2665
+ raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
2666
+ let data = {};
2667
+ try {
2668
+ data = JSON.parse(raw);
2669
+ } catch {
2670
+ data = { oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear.", porQueProvavelmenteFalhou: [], oQueFazerAgora: [], sugestaoCorrecao: null, conceito: null, framework: fw };
2671
+ }
2672
+ data.framework = data.framework || fw;
2673
+ const oneLine = oneLineFailureSummary(resolvedOutput, fw, data.oQueAconteceu, data.sugestaoCorrecao);
2674
+ const formattedText = formatFailureExplanation(data, data.resumoEmUmaFrase || oneLine);
2675
+ return { ok: true, formattedText, structuredContent: { ...data, formattedText } };
2676
+ } catch (err) {
2677
+ return { ok: false, error: err.message, structuredContent: null };
2678
+ }
2679
+ }
1737
2680
  server.registerTool(
1738
2681
  "por_que_falhou",
1739
2682
  {
@@ -1756,14 +2699,12 @@ server.registerTool(
1756
2699
  })
1757
2700
  },
1758
2701
  async ({ errorOutput, testFilePath }) => {
1759
- const structure = detectProjectStructure();
1760
- const fw = structure.testFrameworks[0] || "unknown";
1761
2702
  let resolvedOutput = errorOutput?.trim() || "";
1762
2703
  if (!resolvedOutput) {
1763
- const lastFailurePath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1764
- if (fs5.existsSync(lastFailurePath)) {
2704
+ const lastFailurePath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
2705
+ if (fs6.existsSync(lastFailurePath)) {
1765
2706
  try {
1766
- resolvedOutput = fs5.readFileSync(lastFailurePath, "utf8");
2707
+ resolvedOutput = fs6.readFileSync(lastFailurePath, "utf8");
1767
2708
  } catch {
1768
2709
  }
1769
2710
  }
@@ -1777,94 +2718,36 @@ server.registerTool(
1777
2718
  structuredContent: { ok: false, error: "No error output" }
1778
2719
  };
1779
2720
  }
1780
- let testCode = "";
1781
- if (testFilePath) {
1782
- const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
1783
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
1784
- if (fs5.existsSync(fullPath) && !fs5.statSync(fullPath).isDirectory()) {
1785
- try {
1786
- testCode = fs5.readFileSync(fullPath, "utf8");
1787
- } catch {
1788
- }
1789
- }
1790
- }
1791
- const llm = resolveLLMProvider("complex");
1792
- if (!llm.apiKey) {
1793
- return {
1794
- content: [{
1795
- type: "text",
1796
- text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
1797
- }],
1798
- structuredContent: { ok: false, error: "No API key configured" }
1799
- };
1800
- }
1801
- const { provider, apiKey, baseUrl, model } = llm;
1802
- const fwHints = {
1803
- webdriverio: "WebdriverIO (describe/it, $, browser.$)",
1804
- appium: "Appium/WebdriverIO (mobile, $, browser.$)",
1805
- playwright: "Playwright (test, page, locator)",
1806
- cypress: "Cypress (cy.get, cy.click)",
1807
- jest: "Jest (describe, test, expect)",
1808
- vitest: "Vitest (describe, test, expect)",
1809
- robot: "Robot Framework",
1810
- pytest: "pytest"
1811
- };
1812
- 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:
1813
- - oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
1814
- - porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas, uma por item)
1815
- - oQueFazerAgora: array de strings (passos numerados do que fazer)
1816
- - sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o se aplic\xE1vel, no formato do framework)
1817
- - conceito: string ou null (ex: "Flaky test = teste intermitente. Geralmente por timing ou seletores fr\xE1geis.")
1818
- - framework: string (framework do projeto)
1819
-
1820
- Framework do projeto: ${fw}. ${fwHints[fw] || ""}
1821
- Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
1822
- const userPrompt = `Output do terminal/log (teste falhou):
1823
- ---
1824
- ${resolvedOutput.slice(0, 12e3)}
1825
- ---
1826
- ${testCode ? `
1827
- C\xF3digo do teste que falhou:
1828
- ---
1829
- ${testCode.slice(0, 6e3)}
1830
- ---` : ""}`;
1831
- try {
1832
- let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
1833
- raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
1834
- let data = {};
1835
- try {
1836
- data = JSON.parse(raw);
1837
- } catch {
1838
- data = {
1839
- oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear a resposta.",
1840
- porQueProvavelmenteFalhou: [],
1841
- oQueFazerAgora: [],
1842
- sugestaoCorrecao: null,
1843
- conceito: null,
1844
- framework: fw
2721
+ const explainResult = await generateFailureExplanation(resolvedOutput, testFilePath);
2722
+ if (!explainResult.ok) {
2723
+ if (!resolveLLMProvider("complex").apiKey) {
2724
+ return {
2725
+ content: [{
2726
+ type: "text",
2727
+ text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
2728
+ }],
2729
+ structuredContent: { ok: false, error: "No API key configured" }
1845
2730
  };
1846
2731
  }
1847
- data.framework = data.framework || fw;
1848
- const formattedText = formatFailureExplanation(data);
1849
- return {
1850
- content: [{ type: "text", text: formattedText }],
1851
- structuredContent: {
1852
- ok: true,
1853
- oQueAconteceu: data.oQueAconteceu,
1854
- porQueProvavelmenteFalhou: data.porQueProvavelmenteFalhou,
1855
- oQueFazerAgora: data.oQueFazerAgora,
1856
- sugestaoCorrecao: data.sugestaoCorrecao ?? null,
1857
- conceito: data.conceito ?? null,
1858
- framework: data.framework,
1859
- formattedText
1860
- }
1861
- };
1862
- } catch (err) {
1863
2732
  return {
1864
- content: [{ type: "text", text: `Erro ao analisar: ${err.message}` }],
1865
- structuredContent: { ok: false, error: err.message }
2733
+ content: [{ type: "text", text: `Erro ao analisar: ${explainResult.error || "erro desconhecido"}` }],
2734
+ structuredContent: { ok: false, error: explainResult.error }
1866
2735
  };
1867
2736
  }
2737
+ const sc = explainResult.structuredContent;
2738
+ return {
2739
+ content: [{ type: "text", text: sc.formattedText }],
2740
+ structuredContent: {
2741
+ ok: true,
2742
+ oQueAconteceu: sc.oQueAconteceu,
2743
+ porQueProvavelmenteFalhou: sc.porQueProvavelmenteFalhou,
2744
+ oQueFazerAgora: sc.oQueFazerAgora,
2745
+ sugestaoCorrecao: sc.sugestaoCorrecao ?? null,
2746
+ conceito: sc.conceito ?? null,
2747
+ framework: sc.framework,
2748
+ formattedText: sc.formattedText
2749
+ }
2750
+ };
1868
2751
  }
1869
2752
  );
1870
2753
  server.registerTool(
@@ -1932,7 +2815,7 @@ server.registerTool(
1932
2815
  inputSchema: z.object({
1933
2816
  testFilePath: z.string().describe("Caminho do arquivo de teste que falhou (ex: specs/login.spec.js)."),
1934
2817
  errorOutput: z.string().optional().describe("Output do terminal da falha. Se vazio, l\xEA de .qa-lab-last-failure.log."),
1935
- framework: z.enum(["cypress", "playwright", "webdriverio", "appium"]).optional().describe("Framework do teste. Detectado automaticamente se omitido.")
2818
+ framework: z.enum(["cypress", "playwright", "webdriverio", "appium", "detox"]).optional().describe("Framework do teste. Detectado automaticamente se omitido.")
1936
2819
  }),
1937
2820
  outputSchema: z.object({
1938
2821
  ok: z.boolean(),
@@ -1947,9 +2830,9 @@ server.registerTool(
1947
2830
  const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
1948
2831
  let resolvedOutput = errorOutput;
1949
2832
  if (!resolvedOutput) {
1950
- const logPath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1951
- if (fs5.existsSync(logPath)) {
1952
- resolvedOutput = fs5.readFileSync(logPath, "utf8");
2833
+ const logPath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
2834
+ if (fs6.existsSync(logPath)) {
2835
+ resolvedOutput = fs6.readFileSync(logPath, "utf8");
1953
2836
  }
1954
2837
  }
1955
2838
  if (!resolvedOutput) {
@@ -1965,10 +2848,10 @@ server.registerTool(
1965
2848
  };
1966
2849
  }
1967
2850
  let testCode = "";
1968
- const fullPath = path5.join(PROJECT_ROOT5, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
1969
- if (fs5.existsSync(fullPath)) {
2851
+ const fullPath = path6.join(PROJECT_ROOT6, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
2852
+ if (fs6.existsSync(fullPath)) {
1970
2853
  try {
1971
- testCode = fs5.readFileSync(fullPath, "utf8");
2854
+ testCode = fs6.readFileSync(fullPath, "utf8");
1972
2855
  } catch {
1973
2856
  }
1974
2857
  }
@@ -1984,15 +2867,18 @@ server.registerTool(
1984
2867
  cypress: "Cypress: cy.get('[data-testid=...]'), cy.contains(), cy.get('button').filter(':visible')",
1985
2868
  playwright: `Playwright: page.getByRole(), page.getByTestId(), page.locator('button:has-text("...")')`,
1986
2869
  webdriverio: "WebdriverIO: $('[data-testid=...]'), $('button=Texto')",
1987
- appium: "Appium: $('~accessibility-id'), $('//android.view.View')"
2870
+ appium: `Appium (HIERARQUIA \xDANICA): 1) id: $('~accessibility-id') ou $('~content-desc'). 2) XPath relacional: \xE2ncora est\xE1vel + eixos + TIPO ESPEC\xCDFICO (android.widget.Button, XCUIElementTypeButton). NUNCA use * em XPath \u2014 quebra por timing e m\xFAltiplos matches. Ex: //android.widget.LinearLayout[@resource-id='login_form']/descendant::android.widget.Button[@text='Entrar']. 3) resource-id. Explique a hierarquia.`,
2871
+ detox: `Detox: testID > accessibilityLabel > text. Explique por que \xE9 mais est\xE1vel.`
1988
2872
  };
2873
+ const mobileRules = fw === "appium" || fw === "detox" ? "\n\nMOBILE: 1) id. 2) XPath relacional: \xE2ncora + eixos + TIPO ESPEC\xCDFICO (android.widget.Button, XCUIElementTypeButton). NUNCA use * \u2014 quebra por timing. Ex: //android.widget.LinearLayout[@resource-id='login_form']/descendant::android.widget.Button[@text='Entrar']. 3) resource-id. Explique por que o seletor \xE9 forte." : "";
1989
2874
  const systemPrompt = `Voc\xEA \xE9 um especialista em testes E2E. O teste falhou porque um seletor n\xE3o encontrou o elemento (UI mudou).
1990
2875
  Analise o erro e o c\xF3digo e responda APENAS em JSON (sem markdown) com as chaves:
1991
2876
  - selectorSugerido: string (o novo seletor recomendado, mais resiliente)
1992
2877
  - codigoCorrigido: string (bloco de c\xF3digo completo corrigido, apenas a parte relevante do teste)
1993
- - explicacao: string (breve explica\xE7\xE3o em portugu\xEAs: por que o antigo falhou e por que o novo \xE9 melhor)
2878
+ - explicacao: string (breve explica\xE7\xE3o em portugu\xEAs: por que o antigo falhou e por que o novo \xE9 melhor. Em mobile: mencione a hierarquia de estabilidade)
1994
2879
 
1995
2880
  Priorize nesta ordem: data-testid > role + accessible name > texto vis\xEDvel > estrutura. Evite classes CSS e IDs que mudam.
2881
+ ${mobileRules}
1996
2882
 
1997
2883
  Framework: ${fw}. ${fwHints[fw] || ""}`;
1998
2884
  const userPrompt = `Output do erro:
@@ -2043,6 +2929,114 @@ ${data.codigoCorrigido}
2043
2929
  }
2044
2930
  }
2045
2931
  );
2932
+ server.registerTool(
2933
+ "map_mobile_elements",
2934
+ {
2935
+ title: "Mapear elementos mobile (estrutura para testes)",
2936
+ description: "Gera estrutura/template de elementos para testes mobile. Aceita deep link, appPackage/appActivity (Android) ou bundleId (iOS). Retorna instru\xE7\xF5es para mapear elementos (Appium Inspector, uiautomator) e template para usar em generate_tests. Se elementsJsonPath fornecido, l\xEA arquivo e formata para contexto.",
2937
+ inputSchema: z.object({
2938
+ deepLink: z.string().optional().describe("Deep link do app (ex: meuapp://login). Indica ambiente mobile."),
2939
+ appPackage: z.string().optional().describe("Android: package do app (ex: com.example.app)."),
2940
+ appActivity: z.string().optional().describe("Android: activity principal (ex: .MainActivity)."),
2941
+ bundleId: z.string().optional().describe("iOS: bundle identifier do app."),
2942
+ elementsJsonPath: z.string().optional().describe("Caminho para arquivo JSON com elementos mapeados (id, text, accessibilityId, xpath).")
2943
+ }),
2944
+ outputSchema: z.object({
2945
+ ok: z.boolean(),
2946
+ environment: z.string().optional(),
2947
+ elements: z.array(z.object({
2948
+ id: z.string().optional(),
2949
+ text: z.string().optional(),
2950
+ accessibilityId: z.string().optional(),
2951
+ xpath: z.string().optional(),
2952
+ resourceId: z.string().optional(),
2953
+ className: z.string().optional()
2954
+ })).optional(),
2955
+ instructions: z.string().optional(),
2956
+ contextForGenerate: z.string().optional().describe("Texto formatado para passar em generate_tests como contexto."),
2957
+ error: z.string().optional()
2958
+ })
2959
+ },
2960
+ async ({ deepLink, appPackage, appActivity, bundleId, elementsJsonPath }) => {
2961
+ const hasMobileContext = deepLink || appPackage || bundleId;
2962
+ const elements = [];
2963
+ let instructions = "";
2964
+ let contextForGenerate = "";
2965
+ if (elementsJsonPath) {
2966
+ const fullPath = path6.join(PROJECT_ROOT6, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
2967
+ if (fs6.existsSync(fullPath)) {
2968
+ try {
2969
+ const raw = fs6.readFileSync(fullPath, "utf8");
2970
+ const parsed = JSON.parse(raw);
2971
+ const arr = Array.isArray(parsed) ? parsed : parsed.elements || parsed.items || [];
2972
+ arr.forEach((el) => {
2973
+ elements.push({
2974
+ id: el.id || el.resourceId,
2975
+ text: el.text || el.label,
2976
+ accessibilityId: el.accessibilityId || el["content-desc"] || el.contentDesc,
2977
+ xpath: el.xpath,
2978
+ resourceId: el.resourceId || el.id,
2979
+ className: el.className || el.class
2980
+ });
2981
+ });
2982
+ contextForGenerate = `
2983
+ Elementos mapeados da tela (use para seletores est\xE1veis em Appium/WDIO):
2984
+ ${JSON.stringify(elements, null, 2)}
2985
+ `;
2986
+ } catch (err) {
2987
+ return {
2988
+ content: [{ type: "text", text: `Erro ao ler ${elementsJsonPath}: ${err.message}` }],
2989
+ structuredContent: { ok: false, error: err.message }
2990
+ };
2991
+ }
2992
+ } else {
2993
+ return {
2994
+ content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${elementsJsonPath}` }],
2995
+ structuredContent: { ok: false, error: "File not found" }
2996
+ };
2997
+ }
2998
+ }
2999
+ if (hasMobileContext || elementsJsonPath) {
3000
+ instructions = [
3001
+ "## Como mapear elementos do app mobile",
3002
+ "",
3003
+ "**Android (Appium):**",
3004
+ "- Use Appium Inspector (appium.io) com appPackage/appActivity",
3005
+ "- Ou: `adb shell uiautomator dump` \u2192 analise o XML exportado",
3006
+ "- Priorize: accessibility-id > resource-id > xpath relativo",
3007
+ "",
3008
+ "**iOS (Appium):**",
3009
+ "- Appium Inspector com bundleId",
3010
+ "- Xcode Accessibility Inspector",
3011
+ "- Priorize: accessibility-id > name",
3012
+ "",
3013
+ "**Formato esperado (elements.json):**",
3014
+ "```json",
3015
+ '[{"accessibilityId": "login_btn", "text": "Entrar", "resourceId": "com.app:id/btn"}]',
3016
+ "```",
3017
+ "",
3018
+ "Salve em um arquivo e passe em `elementsJsonPath` na pr\xF3xima chamada."
3019
+ ].join("\n");
3020
+ }
3021
+ const env = deepLink ? "mobile" : appPackage || bundleId ? "mobile" : elements.length ? "mobile" : "unknown";
3022
+ const text = [
3023
+ contextForGenerate && `## Contexto para generate_tests
3024
+ ${contextForGenerate}`,
3025
+ instructions && `## Instru\xE7\xF5es
3026
+ ${instructions}`
3027
+ ].filter(Boolean).join("\n\n");
3028
+ return {
3029
+ content: [{ type: "text", text: text || (hasMobileContext ? `Ambiente: ${env}. ${instructions}` : "Informe deepLink, appPackage ou elementsJsonPath.") }],
3030
+ structuredContent: {
3031
+ ok: true,
3032
+ environment: env,
3033
+ elements: elements.length ? elements : void 0,
3034
+ instructions: instructions || void 0,
3035
+ contextForGenerate: contextForGenerate || void 0
3036
+ }
3037
+ };
3038
+ }
3039
+ );
2046
3040
  server.registerTool(
2047
3041
  "analyze_file_methods",
2048
3042
  {
@@ -2073,20 +3067,20 @@ server.registerTool(
2073
3067
  },
2074
3068
  async ({ path: filePath }) => {
2075
3069
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
2076
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
2077
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
3070
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
3071
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
2078
3072
  return {
2079
3073
  content: [{ type: "text", text: "Caminho fora do projeto." }],
2080
3074
  structuredContent: { ok: false, error: "Path outside project" }
2081
3075
  };
2082
3076
  }
2083
- if (!fs5.existsSync(fullPath)) {
3077
+ if (!fs6.existsSync(fullPath)) {
2084
3078
  return {
2085
3079
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
2086
3080
  structuredContent: { ok: false, error: "File not found" }
2087
3081
  };
2088
3082
  }
2089
- const stat = fs5.statSync(fullPath);
3083
+ const stat = fs6.statSync(fullPath);
2090
3084
  if (stat.isDirectory()) {
2091
3085
  return {
2092
3086
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Informe um arquivo." }],
@@ -2095,7 +3089,7 @@ server.registerTool(
2095
3089
  }
2096
3090
  let fileContent = "";
2097
3091
  try {
2098
- fileContent = fs5.readFileSync(fullPath, "utf8");
3092
+ fileContent = fs6.readFileSync(fullPath, "utf8");
2099
3093
  } catch (err) {
2100
3094
  return {
2101
3095
  content: [{ type: "text", text: `Erro ao ler: ${err.message}` }],
@@ -2113,7 +3107,7 @@ server.registerTool(
2113
3107
  };
2114
3108
  }
2115
3109
  const { provider, apiKey, baseUrl, model } = llm;
2116
- const ext = path5.extname(fullPath).toLowerCase();
3110
+ const ext = path6.extname(fullPath).toLowerCase();
2117
3111
  const lang = [".ts", ".tsx"].includes(ext) ? "TypeScript" : [".js", ".jsx"].includes(ext) ? "JavaScript" : [".py"].includes(ext) ? "Python" : "c\xF3digo";
2118
3112
  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:
2119
3113
 
@@ -2305,9 +3299,9 @@ server.registerTool(
2305
3299
  const msByPeriod = { "7d": 7 * 24 * 60 * 60 * 1e3, "30d": 30 * 24 * 60 * 60 * 1e3, all: Infinity };
2306
3300
  const cutoff = now - msByPeriod[period];
2307
3301
  let data = { events: [] };
2308
- if (fs5.existsSync(METRICS_FILE2)) {
3302
+ if (fs6.existsSync(METRICS_FILE2)) {
2309
3303
  try {
2310
- data = JSON.parse(fs5.readFileSync(METRICS_FILE2, "utf8"));
3304
+ data = JSON.parse(fs6.readFileSync(METRICS_FILE2, "utf8"));
2311
3305
  } catch {
2312
3306
  }
2313
3307
  }
@@ -2344,9 +3338,9 @@ server.registerTool(
2344
3338
  };
2345
3339
  }
2346
3340
  let flowCoverage = null;
2347
- if (fs5.existsSync(FLOWS_CONFIG_FILE)) {
3341
+ if (fs6.existsSync(FLOWS_CONFIG_FILE)) {
2348
3342
  try {
2349
- const flowsConfig = JSON.parse(fs5.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
3343
+ const flowsConfig = JSON.parse(fs6.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
2350
3344
  const flows = flowsConfig.flows || [];
2351
3345
  const structure = detectProjectStructure();
2352
3346
  const allTestFiles = new Set(collectTestFiles(structure).map((e) => e.path));
@@ -2485,7 +3479,7 @@ server.registerTool(
2485
3479
  }
2486
3480
  return new Promise((resolve) => {
2487
3481
  const child = spawn2(cmd, args, {
2488
- cwd: PROJECT_ROOT5,
3482
+ cwd: PROJECT_ROOT6,
2489
3483
  stdio: ["inherit", "pipe", "pipe"],
2490
3484
  shell: process.platform === "win32",
2491
3485
  env: { ...process.env }
@@ -2531,13 +3525,13 @@ server.registerTool(
2531
3525
  async ({ packageManager = "auto" }) => {
2532
3526
  let pm = packageManager;
2533
3527
  if (pm === "auto") {
2534
- if (fs5.existsSync(path5.join(PROJECT_ROOT5, "yarn.lock"))) pm = "yarn";
2535
- else if (fs5.existsSync(path5.join(PROJECT_ROOT5, "pnpm-lock.yaml"))) pm = "pnpm";
3528
+ if (fs6.existsSync(path6.join(PROJECT_ROOT6, "yarn.lock"))) pm = "yarn";
3529
+ else if (fs6.existsSync(path6.join(PROJECT_ROOT6, "pnpm-lock.yaml"))) pm = "pnpm";
2536
3530
  else pm = "npm";
2537
3531
  }
2538
3532
  return new Promise((resolve) => {
2539
3533
  const child = spawn2(pm, ["install"], {
2540
- cwd: PROJECT_ROOT5,
3534
+ cwd: PROJECT_ROOT6,
2541
3535
  stdio: "inherit",
2542
3536
  shell: process.platform === "win32",
2543
3537
  env: { ...process.env }
@@ -2592,9 +3586,9 @@ server.registerTool(
2592
3586
  report += `\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
2593
3587
  `;
2594
3588
  const testFiles = structure.testDirs.flatMap((dir) => {
2595
- const fullPath = path5.join(PROJECT_ROOT5, dir);
2596
- if (!fs5.existsSync(fullPath)) return [];
2597
- return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3589
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3590
+ if (!fs6.existsSync(fullPath)) return [];
3591
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
2598
3592
  });
2599
3593
  report += `\u2705 ${testFiles.length} teste(s) encontrado(s)
2600
3594
 
@@ -2605,7 +3599,7 @@ server.registerTool(
2605
3599
  if (fw) {
2606
3600
  const runResult = await new Promise((resolve) => {
2607
3601
  const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run"], {
2608
- cwd: PROJECT_ROOT5,
3602
+ cwd: PROJECT_ROOT6,
2609
3603
  stdio: ["inherit", "pipe", "pipe"],
2610
3604
  shell: process.platform === "win32"
2611
3605
  });
@@ -2778,9 +3772,9 @@ server.registerTool(
2778
3772
  const memory = loadProjectMemory();
2779
3773
  const stats = getMemoryStats();
2780
3774
  const testFiles = structure.testDirs.flatMap((dir) => {
2781
- const fullPath = path5.join(PROJECT_ROOT5, dir);
2782
- if (!fs5.existsSync(fullPath)) return [];
2783
- return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3775
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3776
+ if (!fs6.existsSync(fullPath)) return [];
3777
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
2784
3778
  });
2785
3779
  let score = 0;
2786
3780
  const recommendations = [];
@@ -2847,9 +3841,9 @@ server.registerTool(
2847
3841
  const memory = loadProjectMemory();
2848
3842
  const suggestions = [];
2849
3843
  const testFiles = structure.testDirs.flatMap((dir) => {
2850
- const fullPath = path5.join(PROJECT_ROOT5, dir);
2851
- if (!fs5.existsSync(fullPath)) return [];
2852
- return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => f.toLowerCase());
3844
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3845
+ if (!fs6.existsSync(fullPath)) return [];
3846
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => f.toLowerCase());
2853
3847
  });
2854
3848
  const criticalFlows = ["login", "logout", "checkout", "payment", "signup", "search"];
2855
3849
  const missingFlows = criticalFlows.filter((flow) => !testFiles.some((f) => f.includes(flow)));
@@ -3011,6 +4005,70 @@ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Us
3011
4005
  };
3012
4006
  }
3013
4007
  );
4008
+ server.registerTool(
4009
+ "get_learning_report",
4010
+ {
4011
+ title: "Relat\xF3rio de evolu\xE7\xE3o e aprendizado",
4012
+ description: "Gera relat\xF3rio de evolu\xE7\xE3o dos aprendizados: resumo por tipo, evolu\xE7\xE3o no tempo e recomenda\xE7\xF5es para aprimorar o c\xF3digo.",
4013
+ inputSchema: z.object({
4014
+ format: z.enum(["summary", "full"]).optional().describe("summary = resumo executivo, full = relat\xF3rio completo com recomenda\xE7\xF5es. Default: summary")
4015
+ }),
4016
+ outputSchema: z.object({
4017
+ summary: z.string(),
4018
+ byType: z.record(z.number()),
4019
+ evolution: z.array(z.object({ date: z.string(), type: z.string(), framework: z.string() })).optional(),
4020
+ recommendations: z.array(z.string()).optional()
4021
+ })
4022
+ },
4023
+ async ({ format = "summary" }) => {
4024
+ const memory = loadProjectMemory();
4025
+ const learnings = memory.learnings || [];
4026
+ const stats = getMemoryStats();
4027
+ const byType = stats.byLearningType || {};
4028
+ const evolution = format === "full" && learnings.length > 0 ? learnings.slice(-30).map((l) => ({
4029
+ date: (l.timestamp || "").slice(0, 10),
4030
+ type: l.type || "unknown",
4031
+ framework: l.framework || "-"
4032
+ })) : [];
4033
+ const recommendations = [];
4034
+ if (byType.element_not_rendered > 0 || byType.element_not_visible > 0) {
4035
+ recommendations.push("Use waits expl\xEDcitos (waitForSelector, waitForDisplayed) ANTES de interagir com elementos.");
4036
+ }
4037
+ if (byType.timing_fix > 0 || byType.element_stale > 0) {
4038
+ recommendations.push("Aumente timeouts e use re-localiza\xE7\xE3o de elementos em listas din\xE2micas.");
4039
+ }
4040
+ if (byType.selector_fix > 0 || byType.mobile_mapping_invisible > 0) {
4041
+ recommendations.push("Priorize data-testid, role e seletores est\xE1veis; em mobile, use mapeamento vis\xEDvel no topo do spec.");
4042
+ }
4043
+ if (stats.firstAttemptSuccessRate < 70 && stats.testsGenerated > 0) {
4044
+ recommendations.push("Aplique UNIVERSAL_TEST_PRACTICES em cada teste gerado: waits inteligentes + assert final.");
4045
+ }
4046
+ if (recommendations.length === 0 && learnings.length > 0) {
4047
+ recommendations.push("Continue aplicando as pr\xE1ticas aprendidas em novos testes.");
4048
+ }
4049
+ const summary = `\u{1F4C8} **Relat\xF3rio de Evolu\xE7\xE3o e Aprendizado**
4050
+
4051
+ **Resumo por tipo:**
4052
+ ${Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => `- ${t}: ${v}`).join("\n") || "- Nenhum aprendizado por tipo ainda"}
4053
+
4054
+ **M\xE9tricas gerais:**
4055
+ - Total de aprendizados: ${stats.totalLearnings}
4056
+ - Taxa de sucesso (1\xAA tentativa): ${stats.firstAttemptSuccessRate}%
4057
+ - Testes gerados: ${stats.testsGenerated}
4058
+
4059
+ ${format === "full" && recommendations.length > 0 ? `**Recomenda\xE7\xF5es para aprimorar o c\xF3digo:**
4060
+ ${recommendations.map((r) => `\u2022 ${r}`).join("\n")}` : ""}`;
4061
+ return {
4062
+ content: [{ type: "text", text: summary }],
4063
+ structuredContent: {
4064
+ summary: summary.trim(),
4065
+ byType,
4066
+ evolution: format === "full" ? evolution : void 0,
4067
+ recommendations: format === "full" ? recommendations : void 0
4068
+ }
4069
+ };
4070
+ }
4071
+ );
3014
4072
  server.registerTool(
3015
4073
  "qa_compare_with_industry",
3016
4074
  {
@@ -3034,9 +4092,9 @@ server.registerTool(
3034
4092
  const structure = detectProjectStructure();
3035
4093
  const stats = getMemoryStats();
3036
4094
  const testFiles = structure.testDirs.flatMap((dir) => {
3037
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3038
- if (!fs5.existsSync(fullPath)) return [];
3039
- return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
4095
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
4096
+ if (!fs6.existsSync(fullPath)) return [];
4097
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3040
4098
  });
3041
4099
  const industryBenchmarks = {
3042
4100
  coverageAvg: "70-80%",
@@ -3103,16 +4161,16 @@ server.registerTool(
3103
4161
  testFiles = [testFile];
3104
4162
  } else {
3105
4163
  testFiles = structure.testDirs.flatMap((dir) => {
3106
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3107
- if (!fs5.existsSync(fullPath)) return [];
3108
- return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => path5.join(dir, f));
4164
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
4165
+ if (!fs6.existsSync(fullPath)) return [];
4166
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => path6.join(dir, f));
3109
4167
  });
3110
4168
  }
3111
4169
  const predictions = [];
3112
4170
  for (const file of testFiles.slice(0, 20)) {
3113
- const fullPath = path5.join(PROJECT_ROOT5, file);
3114
- if (!fs5.existsSync(fullPath)) continue;
3115
- const content = fs5.readFileSync(fullPath, "utf8");
4171
+ const fullPath = path6.join(PROJECT_ROOT6, file);
4172
+ if (!fs6.existsSync(fullPath)) continue;
4173
+ const content = fs6.readFileSync(fullPath, "utf8");
3116
4174
  const reasons = [];
3117
4175
  let riskScore = 0;
3118
4176
  if (/\.(class|id)\s*=|querySelector|\.class-name/i.test(content)) {
@@ -3187,7 +4245,7 @@ server.registerTool(
3187
4245
  if (fw === "jest") {
3188
4246
  return new Promise((resolve) => {
3189
4247
  const child = spawn2("npx", ["jest", "--coverage"], {
3190
- cwd: PROJECT_ROOT5,
4248
+ cwd: PROJECT_ROOT6,
3191
4249
  stdio: ["inherit", "pipe", "pipe"],
3192
4250
  shell: process.platform === "win32",
3193
4251
  env: { ...process.env }
@@ -3303,14 +4361,16 @@ server.registerTool(
3303
4361
  let testFilePath = null;
3304
4362
  let testContent = null;
3305
4363
  let attempt = 0;
4364
+ let appliedLearningFix = false;
3306
4365
  learnings.push({ attempt: 0, action: "detect_project", result: `${structure.testFrameworks.length} framework(s)` });
3307
4366
  for (attempt = 1; attempt <= maxRetries; attempt++) {
3308
4367
  learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
3309
4368
  const { provider, apiKey, baseUrl, model } = llm;
3310
- const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
4369
+ const memoryHints = memory.learnings?.filter((l) => l.fix).slice(-10).map((l) => l.fix).join("\n") || "";
3311
4370
  const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
3312
- ${memoryHints ? `
3313
- Aprendizados anteriores (use como refer\xEAncia):
4371
+ ${UNIVERSAL_TEST_PRACTICES}
4372
+
4373
+ ${memoryHints ? `Aprendizados anteriores (use como refer\xEAncia):
3314
4374
  ${memoryHints.slice(0, 1e3)}` : ""}
3315
4375
  Retorne SOMENTE o c\xF3digo, sem markdown.`;
3316
4376
  const userPrompt = `Contexto:
@@ -3352,15 +4412,15 @@ Framework: ${fw}`;
3352
4412
  const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
3353
4413
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
3354
4414
  const safeName = fileName + ext;
3355
- testFilePath = path5.join(baseDir, safeName);
3356
- if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
4415
+ testFilePath = path6.join(baseDir, safeName);
4416
+ if (!fs6.existsSync(baseDir)) fs6.mkdirSync(baseDir, { recursive: true });
3357
4417
  }
3358
- fs5.writeFileSync(testFilePath, testContent, "utf8");
4418
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
3359
4419
  learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
3360
4420
  learnings.push({ attempt, action: "run_tests", result: "executando..." });
3361
4421
  const runResult = await new Promise((resolve) => {
3362
4422
  const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
3363
- cwd: PROJECT_ROOT5,
4423
+ cwd: PROJECT_ROOT6,
3364
4424
  stdio: ["inherit", "pipe", "pipe"],
3365
4425
  shell: process.platform === "win32"
3366
4426
  });
@@ -3378,12 +4438,15 @@ Framework: ${fw}`;
3378
4438
  saveProjectMemory({
3379
4439
  learnings: [{ type: "test_generated", request, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
3380
4440
  });
4441
+ const learnedAppendix2 = appliedLearningFix ? `
4442
+
4443
+ ${formatLearnedMessageForUser({ runOutput: runResult?.output, fixSummary: "Ajustei o c\xF3digo aplicando waits e valida\xE7\xE3o correta.", framework: fw })}` : "";
3381
4444
  return {
3382
4445
  content: [{ type: "text", text: `\u2705 Teste passou na tentativa ${attempt}!
3383
4446
 
3384
4447
  Arquivo: ${testFilePath}
3385
4448
 
3386
- Aprendizados salvos.` }],
4449
+ Aprendizados salvos.${learnedAppendix2}` }],
3387
4450
  structuredContent: { ok: true, testFilePath, attempts: attempt, finalStatus: "passed", learnings }
3388
4451
  };
3389
4452
  }
@@ -3393,11 +4456,14 @@ Aprendizados salvos.` }],
3393
4456
  saveProjectMemory({
3394
4457
  learnings: [{ type: "test_generated", request, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
3395
4458
  });
4459
+ const learnedAppendix2 = appliedLearningFix ? `
4460
+
4461
+ ${formatLearnedMessageForUser({ runOutput: runResult.output, framework: fw, fixSummary: "Tentei corrigir. Nas pr\xF3ximas execu\xE7\xF5es usarei esse aprendizado desde o in\xEDcio." })}` : "";
3396
4462
  return {
3397
4463
  content: [{ type: "text", text: `\u274C Teste falhou ap\xF3s ${attempt} tentativa(s).
3398
4464
 
3399
4465
  \xDAltimo erro:
3400
- ${runResult.output.slice(0, 500)}` }],
4466
+ ${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
3401
4467
  structuredContent: { ok: false, testFilePath, attempts: attempt, finalStatus: "max_retries", learnings }
3402
4468
  };
3403
4469
  }
@@ -3412,19 +4478,24 @@ ${runResult.output.slice(0, 500)}` }],
3412
4478
  learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
3413
4479
  const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
3414
4480
  testContent = fixedCode;
3415
- fs5.writeFileSync(testFilePath, testContent, "utf8");
4481
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
3416
4482
  learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
3417
4483
  if (flakyAnalysis.isLikelyFlaky) {
4484
+ const inferredPattern = inferFailurePattern(runResult.output, fw);
4485
+ const learningType = inferredPattern?.learningType || (flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix");
4486
+ const learningFix = inferredPattern?.lesson || fixedCode.slice(0, 500);
3418
4487
  saveProjectMemory({
3419
4488
  learnings: [{
3420
- type: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
4489
+ type: learningType,
3421
4490
  request,
3422
4491
  framework: fw,
3423
- fix: fixedCode.slice(0, 500),
4492
+ fix: learningFix,
4493
+ pattern: inferredPattern?.name,
3424
4494
  success: false,
3425
4495
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3426
4496
  }]
3427
4497
  });
4498
+ appliedLearningFix = true;
3428
4499
  }
3429
4500
  } catch (err) {
3430
4501
  learnings.push({ attempt, action: "error", result: err.message });
@@ -3434,8 +4505,11 @@ ${runResult.output.slice(0, 500)}` }],
3434
4505
  };
3435
4506
  }
3436
4507
  }
4508
+ const learnedAppendix = appliedLearningFix ? `
4509
+
4510
+ ${formatLearnedMessageForUser({ fixSummary: "Tentei corrigir. Nas pr\xF3ximas execu\xE7\xF5es usarei esse aprendizado desde o in\xEDcio." })}` : "";
3437
4511
  return {
3438
- content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).` }],
4512
+ content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).${learnedAppendix}` }],
3439
4513
  structuredContent: { ok: false, testFilePath, attempts: maxRetries, finalStatus: "max_retries", learnings }
3440
4514
  };
3441
4515
  }
@@ -3488,9 +4562,16 @@ test.describe('${type.toUpperCase()} Test', () => {
3488
4562
  );
3489
4563
  async function main() {
3490
4564
  const cmd = process.argv[2];
4565
+ if (cmd === "learning-hub") {
4566
+ const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
4567
+ const hubPath = path6.join(__dirname2, "..", "learning-hub", "src", "server.js");
4568
+ const hubUrl2 = pathToFileURL(hubPath).href;
4569
+ await import(hubUrl2);
4570
+ return;
4571
+ }
3491
4572
  if (cmd === "slack-bot") {
3492
- const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
3493
- const slackBotPath = path5.join(__dirname2, "..", "slack-bot", "src", "index.js");
4573
+ const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
4574
+ const slackBotPath = path6.join(__dirname2, "..", "slack-bot", "src", "index.js");
3494
4575
  const slackBotUrl = pathToFileURL(slackBotPath).href;
3495
4576
  await import(slackBotUrl);
3496
4577
  return;