mcp-lab-agent 2.1.6 → 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
@@ -246,6 +246,16 @@ var FAILURE_ANALYSIS_PATTERNS = [
246
246
  learningType: "timing_fix"
247
247
  }
248
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
+ }
249
259
  function inferFailurePattern(runOutput, framework = "") {
250
260
  const output = (runOutput || "").toLowerCase();
251
261
  for (const p of FAILURE_ANALYSIS_PATTERNS) {
@@ -254,10 +264,15 @@ function inferFailurePattern(runOutput, framework = "") {
254
264
  }
255
265
  return null;
256
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`;
257
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:
258
272
  - Use constantes ou Page Object no TOPO do spec: const ELEMENTS = { loginBtn: '~btn_login', ... };
259
273
  - No teste: $(ELEMENTS.loginBtn).click();
260
- - Nunca deixe seletores "invis\xEDveis" (hardcoded inline repetidos). Isso dificulta manuten\xE7\xE3o e causa falhas.`;
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.`;
261
276
  var UNIVERSAL_TEST_PRACTICES = `PR\xC1TICAS OBRIGAT\xD3RIAS em todo teste gerado:
262
277
  1. Esperas inteligentes: ANTES de interagir, verifique que o elemento est\xE1 dispon\xEDvel (waitForDisplayed, waitForExist, waitForSelector)
263
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'))
@@ -519,6 +534,25 @@ function detectProjectStructure() {
519
534
  const hasWebFrameworks = structure.testFrameworks.some((f) => webFrameworks.includes(f));
520
535
  if (hasWebFrameworks) hints.push("web");
521
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
+ }
522
556
  let environment = "web";
523
557
  if (structure.hasMobile && !hasWebFrameworks) environment = "mobile";
524
558
  else if (structure.hasMobile && hasWebFrameworks) environment = "both";
@@ -604,6 +638,61 @@ function matchesFramework(inferred, requested) {
604
638
  if (inferred === requested) return true;
605
639
  return aliases[inferred]?.includes(requested);
606
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
+ }
607
696
  function getFrameworkCwd(structure, preferredDirs) {
608
697
  for (const dir of preferredDirs) {
609
698
  if (structure.testDirs.includes(dir)) {
@@ -642,11 +731,90 @@ function analyzeCodeRisks() {
642
731
  });
643
732
  }
644
733
 
645
- // src/core/tool-helpers.js
734
+ // src/core/llm-call.js
646
735
  import path3 from "path";
647
736
  import fs3 from "fs";
648
737
  var PROJECT_ROOT3 = process.cwd();
649
- 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");
650
818
  function parseTestRunResult(runOutput, exitCode) {
651
819
  let passed = 0;
652
820
  let failed = 0;
@@ -660,8 +828,8 @@ function parseTestRunResult(runOutput, exitCode) {
660
828
  function recordMetricEvent(event) {
661
829
  try {
662
830
  let data = {};
663
- if (fs3.existsSync(METRICS_FILE)) {
664
- const raw = fs3.readFileSync(METRICS_FILE, "utf8");
831
+ if (fs4.existsSync(METRICS_FILE)) {
832
+ const raw = fs4.readFileSync(METRICS_FILE, "utf8");
665
833
  try {
666
834
  data = JSON.parse(raw);
667
835
  } catch {
@@ -671,7 +839,7 @@ function recordMetricEvent(event) {
671
839
  data.events.push({ ...event, timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString() });
672
840
  data.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
673
841
  if (data.events.length > 500) data.events = data.events.slice(-400);
674
- fs3.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
842
+ fs4.writeFileSync(METRICS_FILE, JSON.stringify(data, null, 2), "utf8");
675
843
  } catch {
676
844
  }
677
845
  }
@@ -689,36 +857,12 @@ function extractFailuresFromOutput(runOutput) {
689
857
  }
690
858
  return failures.slice(0, 20);
691
859
  }
692
- function generateFailureExplanation(testCode, runOutput, memory = {}) {
693
- const lines = [];
694
- lines.push("# An\xE1lise de Falha\n");
695
- lines.push("## C\xF3digo do Teste");
696
- lines.push("```");
697
- lines.push(testCode.slice(0, 2e3));
698
- lines.push("```\n");
699
- lines.push("## Output da Execu\xE7\xE3o");
700
- lines.push("```");
701
- lines.push(runOutput.slice(0, 2e3));
702
- lines.push("```\n");
703
- if (memory.learnings && memory.learnings.length > 0) {
704
- lines.push("## Aprendizados Anteriores (\xFAltimos 5)");
705
- memory.learnings.slice(-5).forEach((l) => {
706
- lines.push(`- **${l.type}**: ${l.description || "N/A"}`);
707
- });
708
- lines.push("");
709
- }
710
- lines.push("## Sua Tarefa");
711
- lines.push("1. Identifique a causa raiz da falha");
712
- lines.push("2. Sugira uma corre\xE7\xE3o espec\xEDfica");
713
- lines.push("3. Explique por que essa corre\xE7\xE3o deve funcionar");
714
- return lines.join("\n");
715
- }
716
860
 
717
861
  // src/cli/commands.js
718
- import path4 from "path";
719
- import fs4 from "fs";
862
+ import path5 from "path";
863
+ import fs5 from "fs";
720
864
  import { spawn } from "child_process";
721
- var PROJECT_ROOT4 = process.cwd();
865
+ var PROJECT_ROOT5 = process.cwd();
722
866
  var QA_AGENTS = {
723
867
  autonomous: { desc: "Modo aut\xF4nomo: gera, testa, corrige e aprende", tools: ["qa_auto"] },
724
868
  detection: { desc: "Detecta estrutura, frameworks, testes", tools: ["detect_project", "read_project", "list_test_files"] },
@@ -734,7 +878,7 @@ var QA_AGENTS = {
734
878
  function getExtensionAndBaseDir(fw, structure) {
735
879
  const extMap = { cypress: ".cy.js", playwright: ".spec.js", jest: ".test.js", vitest: ".test.js", robot: ".robot", pytest: ".py" };
736
880
  const ext = extMap[fw] || ".spec.js";
737
- 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");
738
882
  return { ext, baseDir };
739
883
  }
740
884
  async function handleCLI() {
@@ -754,6 +898,9 @@ COMANDOS CLI:
754
898
  auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
755
899
  stats Estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
756
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
757
904
  detect [--json] Detecta frameworks e estrutura
758
905
  route <tarefa> Sugere qual ferramenta usar
759
906
  list Lista ferramentas MCP dispon\xEDveis
@@ -765,6 +912,9 @@ EXEMPLOS:
765
912
  mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
766
913
  mcp-lab-agent auto "login flow" --max-retries 5
767
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
768
918
  mcp-lab-agent detect --json
769
919
 
770
920
  INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
@@ -897,8 +1047,465 @@ ${recommendations.map((r) => ` \u2022 ${r}`).join("\n")}` : ""}
897
1047
  await handleAnalyzeCommand();
898
1048
  return true;
899
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
+ }
900
1062
  return false;
901
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 };
1448
+ }
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}` : ""}
1462
+ `);
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
+ }
1474
+ }
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..." : ""));
1484
+ }
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
+ });
1508
+ }
902
1509
  async function handleAutoCommand() {
903
1510
  const request = process.argv.slice(3).join(" ");
904
1511
  if (!request) {
@@ -979,16 +1586,16 @@ Framework: ${fw}`;
979
1586
  const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
980
1587
  const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
981
1588
  const safeName = fileName + ext;
982
- testFilePath = path4.join(baseDir, safeName);
983
- 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 });
984
1591
  }
985
- fs4.writeFileSync(testFilePath, testContent, "utf8");
1592
+ fs5.writeFileSync(testFilePath, testContent, "utf8");
986
1593
  console.log(`\u2705 Teste gravado: ${testFilePath}`);
987
1594
  console.log(`
988
1595
  [Tentativa ${attempt}/${maxRetries}] Executando teste...`);
989
1596
  const runResult = await new Promise((resolve) => {
990
1597
  const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
991
- cwd: PROJECT_ROOT4,
1598
+ cwd: PROJECT_ROOT5,
992
1599
  stdio: ["inherit", "pipe", "pipe"],
993
1600
  shell: process.platform === "win32"
994
1601
  });
@@ -1055,9 +1662,9 @@ async function handleAnalyzeCommand() {
1055
1662
  console.log(`\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
1056
1663
  `);
1057
1664
  const testFiles = structure.testDirs.flatMap((dir) => {
1058
- const fullPath = path4.join(PROJECT_ROOT4, dir);
1059
- if (!fs4.existsSync(fullPath)) return [];
1060
- 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));
1061
1668
  });
1062
1669
  console.log(`\u2705 ${testFiles.length} teste(s) encontrado(s)
1063
1670
  `);
@@ -1116,13 +1723,13 @@ async function handleAnalyzeCommand() {
1116
1723
  }
1117
1724
 
1118
1725
  // src/index.js
1119
- var PROJECT_ROOT5 = process.cwd();
1120
- config({ path: path5.join(PROJECT_ROOT5, ".env") });
1726
+ var PROJECT_ROOT6 = process.cwd();
1727
+ config({ path: path6.join(PROJECT_ROOT6, ".env") });
1121
1728
  var server = new McpServer({
1122
1729
  name: "mcp-lab-agent",
1123
- version: "2.1.0"
1730
+ version: "2.1.9"
1124
1731
  });
1125
- var METRICS_FILE2 = path5.join(PROJECT_ROOT5, ".qa-lab-metrics.json");
1732
+ var METRICS_FILE2 = path6.join(PROJECT_ROOT6, ".qa-lab-metrics.json");
1126
1733
  function appendMetricsEvent(event) {
1127
1734
  recordMetricEvent(event);
1128
1735
  }
@@ -1143,20 +1750,20 @@ server.registerTool(
1143
1750
  },
1144
1751
  async ({ path: filePath, encoding = "utf8" }) => {
1145
1752
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
1146
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
1147
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
1753
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
1754
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
1148
1755
  return {
1149
1756
  content: [{ type: "text", text: "Caminho fora do projeto." }],
1150
1757
  structuredContent: { ok: false, error: "Path outside project" }
1151
1758
  };
1152
1759
  }
1153
- if (!fs5.existsSync(fullPath)) {
1760
+ if (!fs6.existsSync(fullPath)) {
1154
1761
  return {
1155
1762
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
1156
1763
  structuredContent: { ok: false, error: "File not found" }
1157
1764
  };
1158
1765
  }
1159
- const stat = fs5.statSync(fullPath);
1766
+ const stat = fs6.statSync(fullPath);
1160
1767
  if (stat.isDirectory()) {
1161
1768
  return {
1162
1769
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Use um caminho de arquivo." }],
@@ -1164,7 +1771,7 @@ server.registerTool(
1164
1771
  };
1165
1772
  }
1166
1773
  try {
1167
- const content = fs5.readFileSync(fullPath, encoding);
1774
+ const content = fs6.readFileSync(fullPath, encoding);
1168
1775
  return {
1169
1776
  content: [{ type: "text", text: content }],
1170
1777
  structuredContent: { ok: true, content }
@@ -1248,7 +1855,7 @@ server.registerTool(
1248
1855
  structuredContent: { ok: false, error: "Playwright not installed. Run: npm install playwright" }
1249
1856
  };
1250
1857
  }
1251
- 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");
1252
1859
  const consoleLogs = [];
1253
1860
  const consoleErrors = [];
1254
1861
  const networkRequests = [];
@@ -1275,7 +1882,7 @@ server.registerTool(
1275
1882
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
1276
1883
  await page.screenshot({ path: outPath, fullPage: false });
1277
1884
  await browser.close();
1278
- const relPath = path5.relative(PROJECT_ROOT5, outPath);
1885
+ const relPath = path6.relative(PROJECT_ROOT6, outPath);
1279
1886
  let summary = `Screenshot salvo: ${relPath}`;
1280
1887
  if (consoleErrors.length) summary += `
1281
1888
 
@@ -1397,7 +2004,9 @@ server.registerTool(
1397
2004
  ]).optional().describe("Framework espec\xEDfico ou 'npm' para npm test."),
1398
2005
  spec: z.string().optional().describe("Caminho do spec (ex: cypress/e2e/test.cy.js)."),
1399
2006
  suite: z.string().optional().describe("Suite ou pattern (ex: e2e, api)."),
1400
- 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.")
1401
2010
  }),
1402
2011
  outputSchema: z.object({
1403
2012
  status: z.enum(["passed", "failed", "not_found"]),
@@ -1406,7 +2015,7 @@ server.registerTool(
1406
2015
  runOutput: z.string().optional()
1407
2016
  })
1408
2017
  },
1409
- async ({ framework, spec, suite, explainOnFailure }) => {
2018
+ async ({ framework, spec, suite, explainOnFailure, device, autoFixSelector }) => {
1410
2019
  const structure = detectProjectStructure();
1411
2020
  if (!structure.hasTests) {
1412
2021
  return {
@@ -1422,15 +2031,28 @@ server.registerTool(
1422
2031
  if (!selectedFramework && structure.testFrameworks.length > 0) {
1423
2032
  selectedFramework = structure.testFrameworks[0];
1424
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
+ }
1425
2047
  let cmd, args, cwd;
1426
2048
  if (selectedFramework === "cypress") {
1427
2049
  cmd = "npx";
1428
2050
  args = spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"];
1429
- 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;
1430
2052
  } else if (selectedFramework === "playwright") {
1431
2053
  cmd = "npx";
1432
2054
  args = spec ? ["playwright", "test", spec] : ["playwright", "test"];
1433
- 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;
1434
2056
  } else if (selectedFramework === "webdriverio") {
1435
2057
  cmd = "npx";
1436
2058
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
@@ -1455,108 +2077,138 @@ server.registerTool(
1455
2077
  cmd = "npx";
1456
2078
  args = ["jest"];
1457
2079
  if (spec) args.push(spec);
1458
- cwd = PROJECT_ROOT5;
2080
+ cwd = PROJECT_ROOT6;
1459
2081
  } else if (selectedFramework === "vitest") {
1460
2082
  cmd = "npx";
1461
2083
  args = ["vitest", "run"];
1462
2084
  if (spec) args.push(spec);
1463
- cwd = PROJECT_ROOT5;
2085
+ cwd = PROJECT_ROOT6;
1464
2086
  } else if (selectedFramework === "mocha") {
1465
2087
  cmd = "npx";
1466
2088
  args = spec ? ["mocha", spec] : ["mocha"];
1467
- cwd = PROJECT_ROOT5;
2089
+ cwd = PROJECT_ROOT6;
1468
2090
  } else if (selectedFramework === "appium") {
1469
2091
  cmd = "npx";
1470
2092
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
1471
- cwd = PROJECT_ROOT5;
2093
+ cwd = PROJECT_ROOT6;
1472
2094
  } else if (selectedFramework === "detox") {
1473
2095
  cmd = "npx";
1474
2096
  args = ["detox", "test"];
2097
+ if (useDevice) args.push("--configuration", useDevice);
1475
2098
  if (spec) args.push(spec);
1476
- cwd = PROJECT_ROOT5;
2099
+ cwd = PROJECT_ROOT6;
1477
2100
  } else if (selectedFramework === "robot") {
1478
2101
  cmd = "robot";
1479
2102
  args = spec ? [spec] : [structure.testDirs[0] || "tests"];
1480
- cwd = PROJECT_ROOT5;
2103
+ cwd = PROJECT_ROOT6;
1481
2104
  } else if (selectedFramework === "pytest") {
1482
2105
  cmd = "pytest";
1483
2106
  args = spec ? [spec] : [];
1484
- cwd = PROJECT_ROOT5;
2107
+ cwd = PROJECT_ROOT6;
1485
2108
  } else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
1486
2109
  cmd = "npm";
1487
2110
  args = ["test"];
1488
- cwd = PROJECT_ROOT5;
2111
+ cwd = PROJECT_ROOT6;
1489
2112
  } else {
1490
2113
  cmd = "npm";
1491
2114
  args = ["test"];
1492
- cwd = PROJECT_ROOT5;
2115
+ cwd = PROJECT_ROOT6;
1493
2116
  }
1494
- const startTime = Date.now();
1495
- return new Promise((resolve) => {
2117
+ const runTestsOnce2 = () => new Promise((resolve) => {
2118
+ const startTime = Date.now();
1496
2119
  const child = spawn2(cmd, args, {
1497
2120
  cwd,
1498
2121
  stdio: ["inherit", "pipe", "pipe"],
1499
2122
  shell: process.platform === "win32",
1500
- env: { ...process.env }
2123
+ env: runEnv
1501
2124
  });
1502
2125
  let stdout = "";
1503
2126
  let stderr = "";
1504
- if (child.stdout) {
1505
- child.stdout.on("data", (d) => {
1506
- const s = d.toString();
1507
- stdout += s;
1508
- process.stdout.write(s);
1509
- });
1510
- }
1511
- if (child.stderr) {
1512
- child.stderr.on("data", (d) => {
1513
- const s = d.toString();
1514
- stderr += s;
1515
- process.stderr.write(s);
1516
- });
1517
- }
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
+ });
1518
2135
  child.on("close", (code) => {
1519
2136
  const runOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
1520
- const passed = code === 0;
1521
- const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
1522
- if (!passed && runOutput) {
1523
- try {
1524
- fs5.writeFileSync(path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log"), runOutput, "utf8");
1525
- } catch {
1526
- }
1527
- }
1528
- const { passed: p, failed: f } = parseTestRunResult(runOutput, code);
1529
- appendMetricsEvent({
1530
- type: "test_run",
1531
- framework: selectedFramework,
1532
- spec: spec || void 0,
1533
- passed: p,
1534
- failed: f,
1535
- durationSeconds,
1536
- exitCode: code ?? 1,
1537
- failures: !passed ? extractFailuresFromOutput(runOutput) : void 0
1538
- });
1539
- if (passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
1540
- saveProjectMemory({
1541
- execution: {
1542
- testFile: spec || "all",
1543
- passed,
1544
- duration: durationSeconds,
1545
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1546
- framework: selectedFramework
1547
- }
1548
- });
1549
2137
  resolve({
1550
- content: [{ type: "text", text: passed ? "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes." }],
1551
- structuredContent: {
1552
- status: passed ? "passed" : "failed",
1553
- message: passed ? "Tests passed" : "Tests failed",
1554
- exitCode: code ?? 1,
1555
- runOutput: !passed ? runOutput : void 0
1556
- }
2138
+ passed: code === 0,
2139
+ exitCode: code ?? 1,
2140
+ runOutput,
2141
+ durationSeconds: Math.round((Date.now() - startTime) / 1e3)
1557
2142
  });
1558
2143
  });
1559
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
+ };
1560
2212
  }
1561
2213
  );
1562
2214
  server.registerTool(
@@ -1645,10 +2297,10 @@ server.registerTool(
1645
2297
  ${referenceCode.slice(0, 8e3)}`;
1646
2298
  if (referencePaths?.length) {
1647
2299
  for (const p of referencePaths.slice(0, 5)) {
1648
- const full = path5.join(PROJECT_ROOT5, p.replace(/^\//, "").replace(/\\/g, "/"));
1649
- if (fs5.existsSync(full)) {
2300
+ const full = path6.join(PROJECT_ROOT6, p.replace(/^\//, "").replace(/\\/g, "/"));
2301
+ if (fs6.existsSync(full)) {
1650
2302
  try {
1651
- const content = fs5.readFileSync(full, "utf8");
2303
+ const content = fs6.readFileSync(full, "utf8");
1652
2304
  referenceBlock += `
1653
2305
 
1654
2306
  --- Arquivo: ${p} ---
@@ -1681,7 +2333,9 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
1681
2333
 
1682
2334
  ${UNIVERSAL_TEST_PRACTICES}
1683
2335
  ${fw === "appium" || fw === "detox" ? `
1684
- IMPORTANTE: ${MOBILE_MAPPING_LESSON}` : ""}` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
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.
1685
2339
  Framework: ${fw}
1686
2340
 
1687
2341
  ${UNIVERSAL_TEST_PRACTICES}
@@ -1695,7 +2349,9 @@ Regras:
1695
2349
  - pytest: def test_*, assert, fixtures
1696
2350
  - C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown${fw === "appium" || fw === "detox" ? `
1697
2351
 
1698
- IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}` : ""}`;
2352
+ IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}
2353
+
2354
+ HIERARQUIA: ${MOBILE_SELECTOR_HIERARCHY}` : ""}`;
1699
2355
  const userPrompt = `Contexto do projeto:
1700
2356
  ${contextWithMemory.slice(0, 5e3)}
1701
2357
 
@@ -1779,7 +2435,7 @@ function getExtensionAndBaseDir2(fw, structure) {
1779
2435
  robot: structure.testDirs.includes("robot") ? "robot" : structure.testDirs[0] || "tests",
1780
2436
  behave: structure.testDirs.includes("features") ? "features" : structure.testDirs[0] || "tests"
1781
2437
  };
1782
- 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");
1783
2439
  return { ext, baseDir };
1784
2440
  }
1785
2441
  server.registerTool(
@@ -1824,13 +2480,13 @@ server.registerTool(
1824
2480
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
1825
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, "");
1826
2482
  const fileName = ext.startsWith("_") ? `${safeName}${ext}` : `${safeName}${ext}`;
1827
- const targetDir = subdir ? path5.join(baseDir, subdir) : baseDir;
1828
- const filePath = path5.join(targetDir, fileName);
2483
+ const targetDir = subdir ? path6.join(baseDir, subdir) : baseDir;
2484
+ const filePath = path6.join(targetDir, fileName);
1829
2485
  try {
1830
- if (!fs5.existsSync(targetDir)) {
1831
- fs5.mkdirSync(targetDir, { recursive: true });
2486
+ if (!fs6.existsSync(targetDir)) {
2487
+ fs6.mkdirSync(targetDir, { recursive: true });
1832
2488
  }
1833
- fs5.writeFileSync(filePath, content, "utf8");
2489
+ fs6.writeFileSync(filePath, content, "utf8");
1834
2490
  return {
1835
2491
  content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
1836
2492
  structuredContent: { ok: true, path: filePath }
@@ -1900,8 +2556,10 @@ server.registerTool(
1900
2556
  };
1901
2557
  }
1902
2558
  );
1903
- function formatFailureExplanation(data) {
1904
- const lines = [
2559
+ function formatFailureExplanation(data, oneLine = null) {
2560
+ const summary = oneLine || data.resumoEmUmaFrase || "";
2561
+ const lines = summary ? [`**${summary}**`, "", "---", ""] : [];
2562
+ lines.push(
1905
2563
  "## O que aconteceu",
1906
2564
  "",
1907
2565
  data.oQueAconteceu || "",
@@ -1913,7 +2571,7 @@ function formatFailureExplanation(data) {
1913
2571
  "## O que fazer agora",
1914
2572
  "",
1915
2573
  ...Array.isArray(data.oQueFazerAgora) ? data.oQueFazerAgora.map((s, i) => `${i + 1}. ${s}`) : [data.oQueFazerAgora || ""]
1916
- ];
2574
+ );
1917
2575
  if (data.sugestaoCorrecao) {
1918
2576
  lines.push("", "## Sugest\xE3o de corre\xE7\xE3o", "", "```" + (data.framework || "js"), data.sugestaoCorrecao, "```");
1919
2577
  }
@@ -1955,6 +2613,70 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
1955
2613
  const data = await res.json();
1956
2614
  return data.choices?.[0]?.message?.content || "";
1957
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
+ }
1958
2680
  server.registerTool(
1959
2681
  "por_que_falhou",
1960
2682
  {
@@ -1977,14 +2699,12 @@ server.registerTool(
1977
2699
  })
1978
2700
  },
1979
2701
  async ({ errorOutput, testFilePath }) => {
1980
- const structure = detectProjectStructure();
1981
- const fw = structure.testFrameworks[0] || "unknown";
1982
2702
  let resolvedOutput = errorOutput?.trim() || "";
1983
2703
  if (!resolvedOutput) {
1984
- const lastFailurePath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1985
- if (fs5.existsSync(lastFailurePath)) {
2704
+ const lastFailurePath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
2705
+ if (fs6.existsSync(lastFailurePath)) {
1986
2706
  try {
1987
- resolvedOutput = fs5.readFileSync(lastFailurePath, "utf8");
2707
+ resolvedOutput = fs6.readFileSync(lastFailurePath, "utf8");
1988
2708
  } catch {
1989
2709
  }
1990
2710
  }
@@ -1998,94 +2718,36 @@ server.registerTool(
1998
2718
  structuredContent: { ok: false, error: "No error output" }
1999
2719
  };
2000
2720
  }
2001
- let testCode = "";
2002
- if (testFilePath) {
2003
- const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
2004
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
2005
- if (fs5.existsSync(fullPath) && !fs5.statSync(fullPath).isDirectory()) {
2006
- try {
2007
- testCode = fs5.readFileSync(fullPath, "utf8");
2008
- } catch {
2009
- }
2010
- }
2011
- }
2012
- const llm = resolveLLMProvider("complex");
2013
- if (!llm.apiKey) {
2014
- return {
2015
- content: [{
2016
- type: "text",
2017
- text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
2018
- }],
2019
- structuredContent: { ok: false, error: "No API key configured" }
2020
- };
2021
- }
2022
- const { provider, apiKey, baseUrl, model } = llm;
2023
- const fwHints = {
2024
- webdriverio: "WebdriverIO (describe/it, $, browser.$)",
2025
- appium: "Appium/WebdriverIO (mobile, $, browser.$)",
2026
- playwright: "Playwright (test, page, locator)",
2027
- cypress: "Cypress (cy.get, cy.click)",
2028
- jest: "Jest (describe, test, expect)",
2029
- vitest: "Vitest (describe, test, expect)",
2030
- robot: "Robot Framework",
2031
- pytest: "pytest"
2032
- };
2033
- 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:
2034
- - oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
2035
- - porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas, uma por item)
2036
- - oQueFazerAgora: array de strings (passos numerados do que fazer)
2037
- - sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o se aplic\xE1vel, no formato do framework)
2038
- - conceito: string ou null (ex: "Flaky test = teste intermitente. Geralmente por timing ou seletores fr\xE1geis.")
2039
- - framework: string (framework do projeto)
2040
-
2041
- Framework do projeto: ${fw}. ${fwHints[fw] || ""}
2042
- Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
2043
- const userPrompt = `Output do terminal/log (teste falhou):
2044
- ---
2045
- ${resolvedOutput.slice(0, 12e3)}
2046
- ---
2047
- ${testCode ? `
2048
- C\xF3digo do teste que falhou:
2049
- ---
2050
- ${testCode.slice(0, 6e3)}
2051
- ---` : ""}`;
2052
- try {
2053
- let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
2054
- raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
2055
- let data = {};
2056
- try {
2057
- data = JSON.parse(raw);
2058
- } catch {
2059
- data = {
2060
- oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear a resposta.",
2061
- porQueProvavelmenteFalhou: [],
2062
- oQueFazerAgora: [],
2063
- sugestaoCorrecao: null,
2064
- conceito: null,
2065
- 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" }
2066
2730
  };
2067
2731
  }
2068
- data.framework = data.framework || fw;
2069
- const formattedText = formatFailureExplanation(data);
2070
2732
  return {
2071
- content: [{ type: "text", text: formattedText }],
2072
- structuredContent: {
2073
- ok: true,
2074
- oQueAconteceu: data.oQueAconteceu,
2075
- porQueProvavelmenteFalhou: data.porQueProvavelmenteFalhou,
2076
- oQueFazerAgora: data.oQueFazerAgora,
2077
- sugestaoCorrecao: data.sugestaoCorrecao ?? null,
2078
- conceito: data.conceito ?? null,
2079
- framework: data.framework,
2080
- formattedText
2081
- }
2082
- };
2083
- } catch (err) {
2084
- return {
2085
- content: [{ type: "text", text: `Erro ao analisar: ${err.message}` }],
2086
- 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 }
2087
2735
  };
2088
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
+ };
2089
2751
  }
2090
2752
  );
2091
2753
  server.registerTool(
@@ -2153,7 +2815,7 @@ server.registerTool(
2153
2815
  inputSchema: z.object({
2154
2816
  testFilePath: z.string().describe("Caminho do arquivo de teste que falhou (ex: specs/login.spec.js)."),
2155
2817
  errorOutput: z.string().optional().describe("Output do terminal da falha. Se vazio, l\xEA de .qa-lab-last-failure.log."),
2156
- 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.")
2157
2819
  }),
2158
2820
  outputSchema: z.object({
2159
2821
  ok: z.boolean(),
@@ -2168,9 +2830,9 @@ server.registerTool(
2168
2830
  const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
2169
2831
  let resolvedOutput = errorOutput;
2170
2832
  if (!resolvedOutput) {
2171
- const logPath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
2172
- if (fs5.existsSync(logPath)) {
2173
- 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");
2174
2836
  }
2175
2837
  }
2176
2838
  if (!resolvedOutput) {
@@ -2186,10 +2848,10 @@ server.registerTool(
2186
2848
  };
2187
2849
  }
2188
2850
  let testCode = "";
2189
- const fullPath = path5.join(PROJECT_ROOT5, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
2190
- if (fs5.existsSync(fullPath)) {
2851
+ const fullPath = path6.join(PROJECT_ROOT6, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
2852
+ if (fs6.existsSync(fullPath)) {
2191
2853
  try {
2192
- testCode = fs5.readFileSync(fullPath, "utf8");
2854
+ testCode = fs6.readFileSync(fullPath, "utf8");
2193
2855
  } catch {
2194
2856
  }
2195
2857
  }
@@ -2205,15 +2867,18 @@ server.registerTool(
2205
2867
  cypress: "Cypress: cy.get('[data-testid=...]'), cy.contains(), cy.get('button').filter(':visible')",
2206
2868
  playwright: `Playwright: page.getByRole(), page.getByTestId(), page.locator('button:has-text("...")')`,
2207
2869
  webdriverio: "WebdriverIO: $('[data-testid=...]'), $('button=Texto')",
2208
- 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.`
2209
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." : "";
2210
2874
  const systemPrompt = `Voc\xEA \xE9 um especialista em testes E2E. O teste falhou porque um seletor n\xE3o encontrou o elemento (UI mudou).
2211
2875
  Analise o erro e o c\xF3digo e responda APENAS em JSON (sem markdown) com as chaves:
2212
2876
  - selectorSugerido: string (o novo seletor recomendado, mais resiliente)
2213
2877
  - codigoCorrigido: string (bloco de c\xF3digo completo corrigido, apenas a parte relevante do teste)
2214
- - 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)
2215
2879
 
2216
2880
  Priorize nesta ordem: data-testid > role + accessible name > texto vis\xEDvel > estrutura. Evite classes CSS e IDs que mudam.
2881
+ ${mobileRules}
2217
2882
 
2218
2883
  Framework: ${fw}. ${fwHints[fw] || ""}`;
2219
2884
  const userPrompt = `Output do erro:
@@ -2298,10 +2963,10 @@ server.registerTool(
2298
2963
  let instructions = "";
2299
2964
  let contextForGenerate = "";
2300
2965
  if (elementsJsonPath) {
2301
- const fullPath = path5.join(PROJECT_ROOT5, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
2302
- if (fs5.existsSync(fullPath)) {
2966
+ const fullPath = path6.join(PROJECT_ROOT6, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
2967
+ if (fs6.existsSync(fullPath)) {
2303
2968
  try {
2304
- const raw = fs5.readFileSync(fullPath, "utf8");
2969
+ const raw = fs6.readFileSync(fullPath, "utf8");
2305
2970
  const parsed = JSON.parse(raw);
2306
2971
  const arr = Array.isArray(parsed) ? parsed : parsed.elements || parsed.items || [];
2307
2972
  arr.forEach((el) => {
@@ -2402,20 +3067,20 @@ server.registerTool(
2402
3067
  },
2403
3068
  async ({ path: filePath }) => {
2404
3069
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
2405
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
2406
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
3070
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
3071
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
2407
3072
  return {
2408
3073
  content: [{ type: "text", text: "Caminho fora do projeto." }],
2409
3074
  structuredContent: { ok: false, error: "Path outside project" }
2410
3075
  };
2411
3076
  }
2412
- if (!fs5.existsSync(fullPath)) {
3077
+ if (!fs6.existsSync(fullPath)) {
2413
3078
  return {
2414
3079
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
2415
3080
  structuredContent: { ok: false, error: "File not found" }
2416
3081
  };
2417
3082
  }
2418
- const stat = fs5.statSync(fullPath);
3083
+ const stat = fs6.statSync(fullPath);
2419
3084
  if (stat.isDirectory()) {
2420
3085
  return {
2421
3086
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Informe um arquivo." }],
@@ -2424,7 +3089,7 @@ server.registerTool(
2424
3089
  }
2425
3090
  let fileContent = "";
2426
3091
  try {
2427
- fileContent = fs5.readFileSync(fullPath, "utf8");
3092
+ fileContent = fs6.readFileSync(fullPath, "utf8");
2428
3093
  } catch (err) {
2429
3094
  return {
2430
3095
  content: [{ type: "text", text: `Erro ao ler: ${err.message}` }],
@@ -2442,7 +3107,7 @@ server.registerTool(
2442
3107
  };
2443
3108
  }
2444
3109
  const { provider, apiKey, baseUrl, model } = llm;
2445
- const ext = path5.extname(fullPath).toLowerCase();
3110
+ const ext = path6.extname(fullPath).toLowerCase();
2446
3111
  const lang = [".ts", ".tsx"].includes(ext) ? "TypeScript" : [".js", ".jsx"].includes(ext) ? "JavaScript" : [".py"].includes(ext) ? "Python" : "c\xF3digo";
2447
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:
2448
3113
 
@@ -2634,9 +3299,9 @@ server.registerTool(
2634
3299
  const msByPeriod = { "7d": 7 * 24 * 60 * 60 * 1e3, "30d": 30 * 24 * 60 * 60 * 1e3, all: Infinity };
2635
3300
  const cutoff = now - msByPeriod[period];
2636
3301
  let data = { events: [] };
2637
- if (fs5.existsSync(METRICS_FILE2)) {
3302
+ if (fs6.existsSync(METRICS_FILE2)) {
2638
3303
  try {
2639
- data = JSON.parse(fs5.readFileSync(METRICS_FILE2, "utf8"));
3304
+ data = JSON.parse(fs6.readFileSync(METRICS_FILE2, "utf8"));
2640
3305
  } catch {
2641
3306
  }
2642
3307
  }
@@ -2673,9 +3338,9 @@ server.registerTool(
2673
3338
  };
2674
3339
  }
2675
3340
  let flowCoverage = null;
2676
- if (fs5.existsSync(FLOWS_CONFIG_FILE)) {
3341
+ if (fs6.existsSync(FLOWS_CONFIG_FILE)) {
2677
3342
  try {
2678
- const flowsConfig = JSON.parse(fs5.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
3343
+ const flowsConfig = JSON.parse(fs6.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
2679
3344
  const flows = flowsConfig.flows || [];
2680
3345
  const structure = detectProjectStructure();
2681
3346
  const allTestFiles = new Set(collectTestFiles(structure).map((e) => e.path));
@@ -2814,7 +3479,7 @@ server.registerTool(
2814
3479
  }
2815
3480
  return new Promise((resolve) => {
2816
3481
  const child = spawn2(cmd, args, {
2817
- cwd: PROJECT_ROOT5,
3482
+ cwd: PROJECT_ROOT6,
2818
3483
  stdio: ["inherit", "pipe", "pipe"],
2819
3484
  shell: process.platform === "win32",
2820
3485
  env: { ...process.env }
@@ -2860,13 +3525,13 @@ server.registerTool(
2860
3525
  async ({ packageManager = "auto" }) => {
2861
3526
  let pm = packageManager;
2862
3527
  if (pm === "auto") {
2863
- if (fs5.existsSync(path5.join(PROJECT_ROOT5, "yarn.lock"))) pm = "yarn";
2864
- 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";
2865
3530
  else pm = "npm";
2866
3531
  }
2867
3532
  return new Promise((resolve) => {
2868
3533
  const child = spawn2(pm, ["install"], {
2869
- cwd: PROJECT_ROOT5,
3534
+ cwd: PROJECT_ROOT6,
2870
3535
  stdio: "inherit",
2871
3536
  shell: process.platform === "win32",
2872
3537
  env: { ...process.env }
@@ -2921,9 +3586,9 @@ server.registerTool(
2921
3586
  report += `\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
2922
3587
  `;
2923
3588
  const testFiles = structure.testDirs.flatMap((dir) => {
2924
- const fullPath = path5.join(PROJECT_ROOT5, dir);
2925
- if (!fs5.existsSync(fullPath)) return [];
2926
- 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));
2927
3592
  });
2928
3593
  report += `\u2705 ${testFiles.length} teste(s) encontrado(s)
2929
3594
 
@@ -2934,7 +3599,7 @@ server.registerTool(
2934
3599
  if (fw) {
2935
3600
  const runResult = await new Promise((resolve) => {
2936
3601
  const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run"], {
2937
- cwd: PROJECT_ROOT5,
3602
+ cwd: PROJECT_ROOT6,
2938
3603
  stdio: ["inherit", "pipe", "pipe"],
2939
3604
  shell: process.platform === "win32"
2940
3605
  });
@@ -3107,9 +3772,9 @@ server.registerTool(
3107
3772
  const memory = loadProjectMemory();
3108
3773
  const stats = getMemoryStats();
3109
3774
  const testFiles = structure.testDirs.flatMap((dir) => {
3110
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3111
- if (!fs5.existsSync(fullPath)) return [];
3112
- 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));
3113
3778
  });
3114
3779
  let score = 0;
3115
3780
  const recommendations = [];
@@ -3176,9 +3841,9 @@ server.registerTool(
3176
3841
  const memory = loadProjectMemory();
3177
3842
  const suggestions = [];
3178
3843
  const testFiles = structure.testDirs.flatMap((dir) => {
3179
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3180
- if (!fs5.existsSync(fullPath)) return [];
3181
- 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());
3182
3847
  });
3183
3848
  const criticalFlows = ["login", "logout", "checkout", "payment", "signup", "search"];
3184
3849
  const missingFlows = criticalFlows.filter((flow) => !testFiles.some((f) => f.includes(flow)));
@@ -3427,9 +4092,9 @@ server.registerTool(
3427
4092
  const structure = detectProjectStructure();
3428
4093
  const stats = getMemoryStats();
3429
4094
  const testFiles = structure.testDirs.flatMap((dir) => {
3430
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3431
- if (!fs5.existsSync(fullPath)) return [];
3432
- 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));
3433
4098
  });
3434
4099
  const industryBenchmarks = {
3435
4100
  coverageAvg: "70-80%",
@@ -3496,16 +4161,16 @@ server.registerTool(
3496
4161
  testFiles = [testFile];
3497
4162
  } else {
3498
4163
  testFiles = structure.testDirs.flatMap((dir) => {
3499
- const fullPath = path5.join(PROJECT_ROOT5, dir);
3500
- if (!fs5.existsSync(fullPath)) return [];
3501
- 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));
3502
4167
  });
3503
4168
  }
3504
4169
  const predictions = [];
3505
4170
  for (const file of testFiles.slice(0, 20)) {
3506
- const fullPath = path5.join(PROJECT_ROOT5, file);
3507
- if (!fs5.existsSync(fullPath)) continue;
3508
- 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");
3509
4174
  const reasons = [];
3510
4175
  let riskScore = 0;
3511
4176
  if (/\.(class|id)\s*=|querySelector|\.class-name/i.test(content)) {
@@ -3580,7 +4245,7 @@ server.registerTool(
3580
4245
  if (fw === "jest") {
3581
4246
  return new Promise((resolve) => {
3582
4247
  const child = spawn2("npx", ["jest", "--coverage"], {
3583
- cwd: PROJECT_ROOT5,
4248
+ cwd: PROJECT_ROOT6,
3584
4249
  stdio: ["inherit", "pipe", "pipe"],
3585
4250
  shell: process.platform === "win32",
3586
4251
  env: { ...process.env }
@@ -3747,15 +4412,15 @@ Framework: ${fw}`;
3747
4412
  const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
3748
4413
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
3749
4414
  const safeName = fileName + ext;
3750
- testFilePath = path5.join(baseDir, safeName);
3751
- 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 });
3752
4417
  }
3753
- fs5.writeFileSync(testFilePath, testContent, "utf8");
4418
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
3754
4419
  learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
3755
4420
  learnings.push({ attempt, action: "run_tests", result: "executando..." });
3756
4421
  const runResult = await new Promise((resolve) => {
3757
4422
  const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
3758
- cwd: PROJECT_ROOT5,
4423
+ cwd: PROJECT_ROOT6,
3759
4424
  stdio: ["inherit", "pipe", "pipe"],
3760
4425
  shell: process.platform === "win32"
3761
4426
  });
@@ -3813,7 +4478,7 @@ ${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
3813
4478
  learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
3814
4479
  const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
3815
4480
  testContent = fixedCode;
3816
- fs5.writeFileSync(testFilePath, testContent, "utf8");
4481
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
3817
4482
  learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
3818
4483
  if (flakyAnalysis.isLikelyFlaky) {
3819
4484
  const inferredPattern = inferFailurePattern(runResult.output, fw);
@@ -3898,15 +4563,15 @@ test.describe('${type.toUpperCase()} Test', () => {
3898
4563
  async function main() {
3899
4564
  const cmd = process.argv[2];
3900
4565
  if (cmd === "learning-hub") {
3901
- const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
3902
- const hubPath = path5.join(__dirname2, "..", "learning-hub", "src", "server.js");
4566
+ const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
4567
+ const hubPath = path6.join(__dirname2, "..", "learning-hub", "src", "server.js");
3903
4568
  const hubUrl2 = pathToFileURL(hubPath).href;
3904
4569
  await import(hubUrl2);
3905
4570
  return;
3906
4571
  }
3907
4572
  if (cmd === "slack-bot") {
3908
- const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
3909
- 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");
3910
4575
  const slackBotUrl = pathToFileURL(slackBotPath).href;
3911
4576
  await import(slackBotUrl);
3912
4577
  return;