mcp-lab-agent 2.1.6 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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) {
@@ -935,16 +1542,19 @@ async function handleAutoCommand() {
935
1542
  [Tentativa ${attempt}/${maxRetries}] Gerando teste...`);
936
1543
  const { provider, apiKey, baseUrl, model } = llm;
937
1544
  const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
1545
+ const packageInfo = structure.packageJson || {};
1546
+ const isESM = packageInfo.type === "module";
938
1547
  const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
939
1548
  ${memoryHints ? `
940
1549
  Aprendizados anteriores (use como refer\xEAncia):
941
1550
  ${memoryHints.slice(0, 1e3)}` : ""}
1551
+ ${isESM ? "\nIMPORTANTE: Use sintaxe ESM (import/export), N\xC3O use require()." : ""}
942
1552
  Retorne SOMENTE o c\xF3digo, sem markdown.`;
943
1553
  const userPrompt = `Contexto:
944
1554
  ${contextLines}
945
1555
 
946
1556
  Gere teste para: ${cleanRequest}
947
- Framework: ${fw}`;
1557
+ Framework: ${fw}${isESM ? "\nUse import { test, expect } from '@playwright/test';" : ""}`;
948
1558
  try {
949
1559
  let specContent = "";
950
1560
  if (provider === "gemini") {
@@ -958,6 +1568,9 @@ Framework: ${fw}`;
958
1568
  })
959
1569
  });
960
1570
  const data = await res.json();
1571
+ if (data.error) {
1572
+ throw new Error(`Gemini API Error: ${data.error.message || JSON.stringify(data.error)}`);
1573
+ }
961
1574
  specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
962
1575
  } else {
963
1576
  const res = await fetch(`${baseUrl}/chat/completions`, {
@@ -971,24 +1584,39 @@ Framework: ${fw}`;
971
1584
  })
972
1585
  });
973
1586
  const data = await res.json();
1587
+ if (data.error) {
1588
+ throw new Error(`API Error: ${data.error.message || JSON.stringify(data.error)}`);
1589
+ }
974
1590
  specContent = data.choices?.[0]?.message?.content || "";
975
1591
  }
1592
+ console.log(`[DEBUG] Resposta do LLM recebida: ${specContent.length} caracteres`);
1593
+ if (!specContent || specContent.trim().length === 0) {
1594
+ throw new Error("LLM retornou conte\xFAdo vazio. Verifique sua API key e conex\xE3o.");
1595
+ }
976
1596
  specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
977
1597
  testContent = specContent;
1598
+ if (!testContent || testContent.trim().length === 0) {
1599
+ throw new Error("Ap\xF3s parsing, o c\xF3digo ficou vazio. Resposta do LLM pode estar em formato inesperado.");
1600
+ }
978
1601
  if (!testFilePath) {
979
1602
  const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
980
1603
  const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
981
1604
  const safeName = fileName + ext;
982
- testFilePath = path4.join(baseDir, safeName);
983
- if (!fs4.existsSync(baseDir)) fs4.mkdirSync(baseDir, { recursive: true });
1605
+ testFilePath = path5.join(baseDir, safeName);
1606
+ if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
1607
+ }
1608
+ fs5.writeFileSync(testFilePath, testContent, "utf8");
1609
+ const fileSize = fs5.statSync(testFilePath).size;
1610
+ if (fileSize === 0) {
1611
+ throw new Error("Arquivo gravado mas est\xE1 vazio. Problema na escrita do arquivo.");
984
1612
  }
985
- fs4.writeFileSync(testFilePath, testContent, "utf8");
986
- console.log(`\u2705 Teste gravado: ${testFilePath}`);
1613
+ console.log(`\u2705 Teste gravado: ${testFilePath} (${fileSize} bytes)`);
987
1614
  console.log(`
988
1615
  [Tentativa ${attempt}/${maxRetries}] Executando teste...`);
1616
+ const runArg = fw === "playwright" ? path5.relative(PROJECT_ROOT5, testFilePath).replace(/\\/g, "/") : testFilePath;
989
1617
  const runResult = await new Promise((resolve) => {
990
- const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
991
- cwd: PROJECT_ROOT4,
1618
+ const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", runArg], {
1619
+ cwd: PROJECT_ROOT5,
992
1620
  stdio: ["inherit", "pipe", "pipe"],
993
1621
  shell: process.platform === "win32"
994
1622
  });
@@ -1034,8 +1662,59 @@ ${runResult.output.slice(0, 800)}
1034
1662
  console.log(`\u26A0\uFE0F Flaky detectado (${flakyAnalysis.confidence.toFixed(2)}): ${flakyAnalysis.patterns.map((p) => p.pattern).join(", ")}`);
1035
1663
  }
1036
1664
  console.log(`
1037
- [Tentativa ${attempt}/${maxRetries}] Aplicando corre\xE7\xE3o (simulada)...`);
1038
- console.log(`\u26A0\uFE0F Corre\xE7\xE3o autom\xE1tica ainda n\xE3o implementada nesta vers\xE3o CLI. Tentando novamente...`);
1665
+ [Tentativa ${attempt}/${maxRetries}] Aplicando corre\xE7\xE3o...`);
1666
+ try {
1667
+ const fixPrompt = `Voc\xEA \xE9 um engenheiro de QA. O teste falhou com o seguinte erro:
1668
+
1669
+ ${runResult.output.slice(0, 1e3)}
1670
+
1671
+ C\xF3digo atual do teste:
1672
+ ${testContent}
1673
+
1674
+ Analise o erro e corrija o teste. Considere:
1675
+ - Seletores podem estar errados (verifique se os elementos existem)
1676
+ - Pode precisar de waits (waitForSelector, waitForLoadState)
1677
+ - Rotas podem estar erradas (/buscar vs /busca)
1678
+ - Elementos podem ter nomes diferentes
1679
+
1680
+ Retorne APENAS o c\xF3digo corrigido, sem explica\xE7\xF5es.${isESM ? "\nUse import { test, expect } from '@playwright/test';" : ""}`;
1681
+ let fixedContent = "";
1682
+ if (provider === "gemini") {
1683
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
1684
+ const res = await fetch(url, {
1685
+ method: "POST",
1686
+ headers: { "Content-Type": "application/json" },
1687
+ body: JSON.stringify({
1688
+ contents: [{ parts: [{ text: fixPrompt }] }],
1689
+ generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
1690
+ })
1691
+ });
1692
+ const data = await res.json();
1693
+ fixedContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
1694
+ } else {
1695
+ const res = await fetch(`${baseUrl}/chat/completions`, {
1696
+ method: "POST",
1697
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1698
+ body: JSON.stringify({
1699
+ model,
1700
+ messages: [{ role: "user", content: fixPrompt }],
1701
+ temperature: 0.3,
1702
+ max_tokens: 4096
1703
+ })
1704
+ });
1705
+ const data = await res.json();
1706
+ fixedContent = data.choices?.[0]?.message?.content || "";
1707
+ }
1708
+ fixedContent = fixedContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1709
+ if (fixedContent && fixedContent.length > 50) {
1710
+ testContent = fixedContent;
1711
+ console.log(`\u2705 Corre\xE7\xE3o gerada pelo LLM.`);
1712
+ } else {
1713
+ console.log(`\u26A0\uFE0F Corre\xE7\xE3o vazia, tentando novamente...`);
1714
+ }
1715
+ } catch (fixErr) {
1716
+ console.log(`\u26A0\uFE0F Erro ao gerar corre\xE7\xE3o: ${fixErr.message}. Tentando novamente...`);
1717
+ }
1039
1718
  } catch (err) {
1040
1719
  console.error(`
1041
1720
  \u274C Erro na tentativa ${attempt}: ${err.message}
@@ -1055,9 +1734,9 @@ async function handleAnalyzeCommand() {
1055
1734
  console.log(`\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
1056
1735
  `);
1057
1736
  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));
1737
+ const fullPath = path5.join(PROJECT_ROOT5, dir);
1738
+ if (!fs5.existsSync(fullPath)) return [];
1739
+ return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
1061
1740
  });
1062
1741
  console.log(`\u2705 ${testFiles.length} teste(s) encontrado(s)
1063
1742
  `);
@@ -1116,13 +1795,13 @@ async function handleAnalyzeCommand() {
1116
1795
  }
1117
1796
 
1118
1797
  // src/index.js
1119
- var PROJECT_ROOT5 = process.cwd();
1120
- config({ path: path5.join(PROJECT_ROOT5, ".env") });
1798
+ var PROJECT_ROOT6 = process.cwd();
1799
+ config({ path: path6.join(PROJECT_ROOT6, ".env") });
1121
1800
  var server = new McpServer({
1122
1801
  name: "mcp-lab-agent",
1123
- version: "2.1.0"
1802
+ version: "2.1.9"
1124
1803
  });
1125
- var METRICS_FILE2 = path5.join(PROJECT_ROOT5, ".qa-lab-metrics.json");
1804
+ var METRICS_FILE2 = path6.join(PROJECT_ROOT6, ".qa-lab-metrics.json");
1126
1805
  function appendMetricsEvent(event) {
1127
1806
  recordMetricEvent(event);
1128
1807
  }
@@ -1143,20 +1822,20 @@ server.registerTool(
1143
1822
  },
1144
1823
  async ({ path: filePath, encoding = "utf8" }) => {
1145
1824
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
1146
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
1147
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
1825
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
1826
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
1148
1827
  return {
1149
1828
  content: [{ type: "text", text: "Caminho fora do projeto." }],
1150
1829
  structuredContent: { ok: false, error: "Path outside project" }
1151
1830
  };
1152
1831
  }
1153
- if (!fs5.existsSync(fullPath)) {
1832
+ if (!fs6.existsSync(fullPath)) {
1154
1833
  return {
1155
1834
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
1156
1835
  structuredContent: { ok: false, error: "File not found" }
1157
1836
  };
1158
1837
  }
1159
- const stat = fs5.statSync(fullPath);
1838
+ const stat = fs6.statSync(fullPath);
1160
1839
  if (stat.isDirectory()) {
1161
1840
  return {
1162
1841
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Use um caminho de arquivo." }],
@@ -1164,7 +1843,7 @@ server.registerTool(
1164
1843
  };
1165
1844
  }
1166
1845
  try {
1167
- const content = fs5.readFileSync(fullPath, encoding);
1846
+ const content = fs6.readFileSync(fullPath, encoding);
1168
1847
  return {
1169
1848
  content: [{ type: "text", text: content }],
1170
1849
  structuredContent: { ok: true, content }
@@ -1248,7 +1927,7 @@ server.registerTool(
1248
1927
  structuredContent: { ok: false, error: "Playwright not installed. Run: npm install playwright" }
1249
1928
  };
1250
1929
  }
1251
- const outPath = screenshotPath ? path5.join(PROJECT_ROOT5, screenshotPath.replace(/^\//, "")) : path5.join(PROJECT_ROOT5, ".qa-lab-screenshot.png");
1930
+ const outPath = screenshotPath ? path6.join(PROJECT_ROOT6, screenshotPath.replace(/^\//, "")) : path6.join(PROJECT_ROOT6, ".qa-lab-screenshot.png");
1252
1931
  const consoleLogs = [];
1253
1932
  const consoleErrors = [];
1254
1933
  const networkRequests = [];
@@ -1275,7 +1954,7 @@ server.registerTool(
1275
1954
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
1276
1955
  await page.screenshot({ path: outPath, fullPage: false });
1277
1956
  await browser.close();
1278
- const relPath = path5.relative(PROJECT_ROOT5, outPath);
1957
+ const relPath = path6.relative(PROJECT_ROOT6, outPath);
1279
1958
  let summary = `Screenshot salvo: ${relPath}`;
1280
1959
  if (consoleErrors.length) summary += `
1281
1960
 
@@ -1397,7 +2076,9 @@ server.registerTool(
1397
2076
  ]).optional().describe("Framework espec\xEDfico ou 'npm' para npm test."),
1398
2077
  spec: z.string().optional().describe("Caminho do spec (ex: cypress/e2e/test.cy.js)."),
1399
2078
  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.")
2079
+ device: z.string().optional().describe("Device/configuration para mobile. Se vazio, detecta de qa-lab-agent.config.json, wdio.conf ou .detoxrc."),
2080
+ 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."),
2081
+ 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
2082
  }),
1402
2083
  outputSchema: z.object({
1403
2084
  status: z.enum(["passed", "failed", "not_found"]),
@@ -1406,7 +2087,7 @@ server.registerTool(
1406
2087
  runOutput: z.string().optional()
1407
2088
  })
1408
2089
  },
1409
- async ({ framework, spec, suite, explainOnFailure }) => {
2090
+ async ({ framework, spec, suite, explainOnFailure, device, autoFixSelector }) => {
1410
2091
  const structure = detectProjectStructure();
1411
2092
  if (!structure.hasTests) {
1412
2093
  return {
@@ -1422,15 +2103,28 @@ server.registerTool(
1422
2103
  if (!selectedFramework && structure.testFrameworks.length > 0) {
1423
2104
  selectedFramework = structure.testFrameworks[0];
1424
2105
  }
2106
+ const deviceConfig = structure.hasMobile ? detectDeviceConfig(structure) : {};
2107
+ const useDevice = device || deviceConfig.configuration || deviceConfig.device;
2108
+ const doAutoFixSelector = autoFixSelector ?? (structure.hasMobile && !!spec);
2109
+ let runEnv = { ...process.env };
2110
+ if (useDevice && Object.keys(deviceConfig.envOverrides || {}).length) {
2111
+ runEnv = { ...runEnv, ...deviceConfig.envOverrides };
2112
+ }
2113
+ if (device) {
2114
+ if (selectedFramework === "detox") runEnv.DETOX_CONFIGURATION = device;
2115
+ else if (selectedFramework === "appium") runEnv.APPIUM_DEVICE_NAME = device;
2116
+ } else if (deviceConfig.configuration && selectedFramework === "detox") {
2117
+ runEnv.DETOX_CONFIGURATION = deviceConfig.configuration;
2118
+ }
1425
2119
  let cmd, args, cwd;
1426
2120
  if (selectedFramework === "cypress") {
1427
2121
  cmd = "npx";
1428
2122
  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;
2123
+ cwd = structure.testDirs.includes("cypress") ? path6.join(PROJECT_ROOT6, "cypress") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
1430
2124
  } else if (selectedFramework === "playwright") {
1431
2125
  cmd = "npx";
1432
2126
  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;
2127
+ cwd = structure.testDirs.includes("playwright") ? path6.join(PROJECT_ROOT6, "playwright") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
1434
2128
  } else if (selectedFramework === "webdriverio") {
1435
2129
  cmd = "npx";
1436
2130
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
@@ -1455,108 +2149,138 @@ server.registerTool(
1455
2149
  cmd = "npx";
1456
2150
  args = ["jest"];
1457
2151
  if (spec) args.push(spec);
1458
- cwd = PROJECT_ROOT5;
2152
+ cwd = PROJECT_ROOT6;
1459
2153
  } else if (selectedFramework === "vitest") {
1460
2154
  cmd = "npx";
1461
2155
  args = ["vitest", "run"];
1462
2156
  if (spec) args.push(spec);
1463
- cwd = PROJECT_ROOT5;
2157
+ cwd = PROJECT_ROOT6;
1464
2158
  } else if (selectedFramework === "mocha") {
1465
2159
  cmd = "npx";
1466
2160
  args = spec ? ["mocha", spec] : ["mocha"];
1467
- cwd = PROJECT_ROOT5;
2161
+ cwd = PROJECT_ROOT6;
1468
2162
  } else if (selectedFramework === "appium") {
1469
2163
  cmd = "npx";
1470
2164
  args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
1471
- cwd = PROJECT_ROOT5;
2165
+ cwd = PROJECT_ROOT6;
1472
2166
  } else if (selectedFramework === "detox") {
1473
2167
  cmd = "npx";
1474
2168
  args = ["detox", "test"];
2169
+ if (useDevice) args.push("--configuration", useDevice);
1475
2170
  if (spec) args.push(spec);
1476
- cwd = PROJECT_ROOT5;
2171
+ cwd = PROJECT_ROOT6;
1477
2172
  } else if (selectedFramework === "robot") {
1478
2173
  cmd = "robot";
1479
2174
  args = spec ? [spec] : [structure.testDirs[0] || "tests"];
1480
- cwd = PROJECT_ROOT5;
2175
+ cwd = PROJECT_ROOT6;
1481
2176
  } else if (selectedFramework === "pytest") {
1482
2177
  cmd = "pytest";
1483
2178
  args = spec ? [spec] : [];
1484
- cwd = PROJECT_ROOT5;
2179
+ cwd = PROJECT_ROOT6;
1485
2180
  } else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
1486
2181
  cmd = "npm";
1487
2182
  args = ["test"];
1488
- cwd = PROJECT_ROOT5;
2183
+ cwd = PROJECT_ROOT6;
1489
2184
  } else {
1490
2185
  cmd = "npm";
1491
2186
  args = ["test"];
1492
- cwd = PROJECT_ROOT5;
2187
+ cwd = PROJECT_ROOT6;
1493
2188
  }
1494
- const startTime = Date.now();
1495
- return new Promise((resolve) => {
2189
+ const runTestsOnce2 = () => new Promise((resolve) => {
2190
+ const startTime = Date.now();
1496
2191
  const child = spawn2(cmd, args, {
1497
2192
  cwd,
1498
2193
  stdio: ["inherit", "pipe", "pipe"],
1499
2194
  shell: process.platform === "win32",
1500
- env: { ...process.env }
2195
+ env: runEnv
1501
2196
  });
1502
2197
  let stdout = "";
1503
2198
  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
- }
2199
+ if (child.stdout) child.stdout.on("data", (d) => {
2200
+ stdout += d.toString();
2201
+ process.stdout.write(d);
2202
+ });
2203
+ if (child.stderr) child.stderr.on("data", (d) => {
2204
+ stderr += d.toString();
2205
+ process.stderr.write(d);
2206
+ });
1518
2207
  child.on("close", (code) => {
1519
2208
  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
2209
  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
- }
2210
+ passed: code === 0,
2211
+ exitCode: code ?? 1,
2212
+ runOutput,
2213
+ durationSeconds: Math.round((Date.now() - startTime) / 1e3)
1557
2214
  });
1558
2215
  });
1559
2216
  });
2217
+ const isSelectorFailure = (out) => /element not found|selector|timeout|locator|cy\.get|page\.locator|Unable to find/i.test(out || "");
2218
+ let result = await runTestsOnce2();
2219
+ let autoFixed = false;
2220
+ if (!result.passed && doAutoFixSelector && spec && isSelectorFailure(result.runOutput) && resolveLLMProvider("complex").apiKey) {
2221
+ const fixResult = await applySelectorFixAndRetry(spec, result.runOutput, selectedFramework);
2222
+ if (fixResult.applied) {
2223
+ autoFixed = true;
2224
+ result = await runTestsOnce2();
2225
+ }
2226
+ }
2227
+ if (!result.passed && result.runOutput) {
2228
+ try {
2229
+ fs6.writeFileSync(path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log"), result.runOutput, "utf8");
2230
+ } catch {
2231
+ }
2232
+ }
2233
+ const { passed: p, failed: f } = parseTestRunResult(result.runOutput, result.exitCode);
2234
+ appendMetricsEvent({
2235
+ type: "test_run",
2236
+ framework: selectedFramework,
2237
+ spec: spec || void 0,
2238
+ passed: p,
2239
+ failed: f,
2240
+ durationSeconds: result.durationSeconds,
2241
+ exitCode: result.exitCode,
2242
+ failures: !result.passed ? extractFailuresFromOutput(result.runOutput) : void 0
2243
+ });
2244
+ if (result.passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
2245
+ saveProjectMemory({
2246
+ execution: {
2247
+ testFile: spec || "all",
2248
+ passed: result.passed,
2249
+ duration: result.durationSeconds,
2250
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2251
+ framework: selectedFramework
2252
+ }
2253
+ });
2254
+ 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.";
2255
+ const structured = {
2256
+ status: result.passed ? "passed" : "failed",
2257
+ message: result.passed ? "Tests passed" : "Tests failed",
2258
+ exitCode: result.exitCode,
2259
+ runOutput: !result.passed ? result.runOutput : void 0,
2260
+ autoFixed: autoFixed || void 0
2261
+ };
2262
+ if (!result.passed && explainOnFailure && result.runOutput) {
2263
+ const explainResult = await generateFailureExplanation(result.runOutput, spec || void 0);
2264
+ if (explainResult.ok && explainResult.structuredContent) {
2265
+ const oneLine = explainResult.structuredContent.resumoEmUmaFrase || oneLineFailureSummary(result.runOutput, selectedFramework, explainResult.structuredContent.oQueAconteceu, explainResult.structuredContent.sugestaoCorrecao);
2266
+ structured.explanation = explainResult.structuredContent.formattedText;
2267
+ structured.resumoEmUmaFrase = oneLine;
2268
+ return {
2269
+ content: [{ type: "text", text: `${baseMsg}
2270
+
2271
+ **${oneLine}**
2272
+
2273
+ ---
2274
+
2275
+ ${explainResult.structuredContent.formattedText}` }],
2276
+ structuredContent: structured
2277
+ };
2278
+ }
2279
+ }
2280
+ return {
2281
+ content: [{ type: "text", text: baseMsg }],
2282
+ structuredContent: structured
2283
+ };
1560
2284
  }
1561
2285
  );
1562
2286
  server.registerTool(
@@ -1645,10 +2369,10 @@ server.registerTool(
1645
2369
  ${referenceCode.slice(0, 8e3)}`;
1646
2370
  if (referencePaths?.length) {
1647
2371
  for (const p of referencePaths.slice(0, 5)) {
1648
- const full = path5.join(PROJECT_ROOT5, p.replace(/^\//, "").replace(/\\/g, "/"));
1649
- if (fs5.existsSync(full)) {
2372
+ const full = path6.join(PROJECT_ROOT6, p.replace(/^\//, "").replace(/\\/g, "/"));
2373
+ if (fs6.existsSync(full)) {
1650
2374
  try {
1651
- const content = fs5.readFileSync(full, "utf8");
2375
+ const content = fs6.readFileSync(full, "utf8");
1652
2376
  referenceBlock += `
1653
2377
 
1654
2378
  --- Arquivo: ${p} ---
@@ -1681,7 +2405,9 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
1681
2405
 
1682
2406
  ${UNIVERSAL_TEST_PRACTICES}
1683
2407
  ${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.
2408
+ IMPORTANTE: ${MOBILE_MAPPING_LESSON}
2409
+
2410
+ 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
2411
  Framework: ${fw}
1686
2412
 
1687
2413
  ${UNIVERSAL_TEST_PRACTICES}
@@ -1695,7 +2421,9 @@ Regras:
1695
2421
  - pytest: def test_*, assert, fixtures
1696
2422
  - C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown${fw === "appium" || fw === "detox" ? `
1697
2423
 
1698
- IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}` : ""}`;
2424
+ IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}
2425
+
2426
+ HIERARQUIA: ${MOBILE_SELECTOR_HIERARCHY}` : ""}`;
1699
2427
  const userPrompt = `Contexto do projeto:
1700
2428
  ${contextWithMemory.slice(0, 5e3)}
1701
2429
 
@@ -1738,8 +2466,18 @@ Framework alvo: ${fw}${referenceBlock}`;
1738
2466
  }
1739
2467
  specContent = specContent.replace(/^```(?:js|javascript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1740
2468
  const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 40);
2469
+ if (!specContent) {
2470
+ return {
2471
+ content: [{ type: "text", text: "Erro: LLM retornou conte\xFAdo vazio. Verifique API key (GROQ_API_KEY, GEMINI_API_KEY) e tente novamente." }],
2472
+ structuredContent: { ok: false, error: "Empty LLM response" }
2473
+ };
2474
+ }
2475
+ const textWithCode = `Spec gerado (${specContent.length} chars). Use write_test para gravar com name="${fileName}" e content abaixo:
2476
+
2477
+ --- C\xF3digo (passe em content para write_test) ---
2478
+ ${specContent}`;
1741
2479
  return {
1742
- content: [{ type: "text", text: `Spec gerado (${specContent.length} chars). Use write_test para gravar.` }],
2480
+ content: [{ type: "text", text: textWithCode }],
1743
2481
  structuredContent: {
1744
2482
  ok: true,
1745
2483
  specContent,
@@ -1779,7 +2517,7 @@ function getExtensionAndBaseDir2(fw, structure) {
1779
2517
  robot: structure.testDirs.includes("robot") ? "robot" : structure.testDirs[0] || "tests",
1780
2518
  behave: structure.testDirs.includes("features") ? "features" : structure.testDirs[0] || "tests"
1781
2519
  };
1782
- const baseDir = path5.join(PROJECT_ROOT5, baseMap[fw] || structure.testDirs[0] || "tests");
2520
+ const baseDir = path6.join(PROJECT_ROOT6, baseMap[fw] || structure.testDirs[0] || "tests");
1783
2521
  return { ext, baseDir };
1784
2522
  }
1785
2523
  server.registerTool(
@@ -1821,16 +2559,22 @@ server.registerTool(
1821
2559
  structuredContent: { ok: false, error: "No test framework" }
1822
2560
  };
1823
2561
  }
2562
+ if (!content || !String(content).trim()) {
2563
+ return {
2564
+ content: [{ type: "text", text: "Erro: content n\xE3o pode ser vazio. Chame generate_tests primeiro e passe o specContent retornado em content." }],
2565
+ structuredContent: { ok: false, error: "Empty content" }
2566
+ };
2567
+ }
1824
2568
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
1825
2569
  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
2570
  const fileName = ext.startsWith("_") ? `${safeName}${ext}` : `${safeName}${ext}`;
1827
- const targetDir = subdir ? path5.join(baseDir, subdir) : baseDir;
1828
- const filePath = path5.join(targetDir, fileName);
2571
+ const targetDir = subdir ? path6.join(baseDir, subdir) : baseDir;
2572
+ const filePath = path6.join(targetDir, fileName);
1829
2573
  try {
1830
- if (!fs5.existsSync(targetDir)) {
1831
- fs5.mkdirSync(targetDir, { recursive: true });
2574
+ if (!fs6.existsSync(targetDir)) {
2575
+ fs6.mkdirSync(targetDir, { recursive: true });
1832
2576
  }
1833
- fs5.writeFileSync(filePath, content, "utf8");
2577
+ fs6.writeFileSync(filePath, content, "utf8");
1834
2578
  return {
1835
2579
  content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
1836
2580
  structuredContent: { ok: true, path: filePath }
@@ -1900,8 +2644,10 @@ server.registerTool(
1900
2644
  };
1901
2645
  }
1902
2646
  );
1903
- function formatFailureExplanation(data) {
1904
- const lines = [
2647
+ function formatFailureExplanation(data, oneLine = null) {
2648
+ const summary = oneLine || data.resumoEmUmaFrase || "";
2649
+ const lines = summary ? [`**${summary}**`, "", "---", ""] : [];
2650
+ lines.push(
1905
2651
  "## O que aconteceu",
1906
2652
  "",
1907
2653
  data.oQueAconteceu || "",
@@ -1913,7 +2659,7 @@ function formatFailureExplanation(data) {
1913
2659
  "## O que fazer agora",
1914
2660
  "",
1915
2661
  ...Array.isArray(data.oQueFazerAgora) ? data.oQueFazerAgora.map((s, i) => `${i + 1}. ${s}`) : [data.oQueFazerAgora || ""]
1916
- ];
2662
+ );
1917
2663
  if (data.sugestaoCorrecao) {
1918
2664
  lines.push("", "## Sugest\xE3o de corre\xE7\xE3o", "", "```" + (data.framework || "js"), data.sugestaoCorrecao, "```");
1919
2665
  }
@@ -1955,6 +2701,70 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
1955
2701
  const data = await res.json();
1956
2702
  return data.choices?.[0]?.message?.content || "";
1957
2703
  }
2704
+ async function generateFailureExplanation(resolvedOutput, testFilePath = null) {
2705
+ const structure = detectProjectStructure();
2706
+ const fw = structure.testFrameworks[0] || "unknown";
2707
+ let testCode = "";
2708
+ if (testFilePath) {
2709
+ const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
2710
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
2711
+ if (fs6.existsSync(fullPath) && !fs6.statSync(fullPath).isDirectory()) {
2712
+ try {
2713
+ testCode = fs6.readFileSync(fullPath, "utf8");
2714
+ } catch {
2715
+ }
2716
+ }
2717
+ }
2718
+ const llm = resolveLLMProvider("complex");
2719
+ if (!llm.apiKey) return { ok: false, structuredContent: null };
2720
+ const { provider, apiKey, baseUrl, model } = llm;
2721
+ const fwHints = {
2722
+ webdriverio: "WebdriverIO (describe/it, $, browser.$)",
2723
+ appium: "Appium/WebdriverIO (mobile, $, browser.$)",
2724
+ playwright: "Playwright (test, page, locator)",
2725
+ cypress: "Cypress (cy.get, cy.click)",
2726
+ jest: "Jest (describe, test, expect)",
2727
+ vitest: "Vitest (describe, test, expect)",
2728
+ robot: "Robot Framework",
2729
+ pytest: "pytest"
2730
+ };
2731
+ 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:
2732
+ - resumoEmUmaFrase: string (OBRIGAT\xD3RIO - uma frase: "Falhou porque X. Solu\xE7\xE3o: Y.")
2733
+ - oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
2734
+ - porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas)
2735
+ - oQueFazerAgora: array de strings (passos numerados do que fazer)
2736
+ - sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o no formato do framework)
2737
+ - conceito: string ou null
2738
+ - framework: string (framework do projeto)
2739
+
2740
+ Framework: ${fw}. ${fwHints[fw] || ""}
2741
+ Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
2742
+ const userPrompt = `Output do terminal/log (teste falhou):
2743
+ ---
2744
+ ${resolvedOutput.slice(0, 12e3)}
2745
+ ---
2746
+ ${testCode ? `
2747
+ C\xF3digo do teste:
2748
+ ---
2749
+ ${testCode.slice(0, 6e3)}
2750
+ ---` : ""}`;
2751
+ try {
2752
+ let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
2753
+ raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
2754
+ let data = {};
2755
+ try {
2756
+ data = JSON.parse(raw);
2757
+ } catch {
2758
+ data = { oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear.", porQueProvavelmenteFalhou: [], oQueFazerAgora: [], sugestaoCorrecao: null, conceito: null, framework: fw };
2759
+ }
2760
+ data.framework = data.framework || fw;
2761
+ const oneLine = oneLineFailureSummary(resolvedOutput, fw, data.oQueAconteceu, data.sugestaoCorrecao);
2762
+ const formattedText = formatFailureExplanation(data, data.resumoEmUmaFrase || oneLine);
2763
+ return { ok: true, formattedText, structuredContent: { ...data, formattedText } };
2764
+ } catch (err) {
2765
+ return { ok: false, error: err.message, structuredContent: null };
2766
+ }
2767
+ }
1958
2768
  server.registerTool(
1959
2769
  "por_que_falhou",
1960
2770
  {
@@ -1977,14 +2787,12 @@ server.registerTool(
1977
2787
  })
1978
2788
  },
1979
2789
  async ({ errorOutput, testFilePath }) => {
1980
- const structure = detectProjectStructure();
1981
- const fw = structure.testFrameworks[0] || "unknown";
1982
2790
  let resolvedOutput = errorOutput?.trim() || "";
1983
2791
  if (!resolvedOutput) {
1984
- const lastFailurePath = path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log");
1985
- if (fs5.existsSync(lastFailurePath)) {
2792
+ const lastFailurePath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
2793
+ if (fs6.existsSync(lastFailurePath)) {
1986
2794
  try {
1987
- resolvedOutput = fs5.readFileSync(lastFailurePath, "utf8");
2795
+ resolvedOutput = fs6.readFileSync(lastFailurePath, "utf8");
1988
2796
  } catch {
1989
2797
  }
1990
2798
  }
@@ -1998,94 +2806,36 @@ server.registerTool(
1998
2806
  structuredContent: { ok: false, error: "No error output" }
1999
2807
  };
2000
2808
  }
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
2809
+ const explainResult = await generateFailureExplanation(resolvedOutput, testFilePath);
2810
+ if (!explainResult.ok) {
2811
+ if (!resolveLLMProvider("complex").apiKey) {
2812
+ return {
2813
+ content: [{
2814
+ type: "text",
2815
+ text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
2816
+ }],
2817
+ structuredContent: { ok: false, error: "No API key configured" }
2066
2818
  };
2067
2819
  }
2068
- data.framework = data.framework || fw;
2069
- const formattedText = formatFailureExplanation(data);
2070
- 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
2820
  return {
2085
- content: [{ type: "text", text: `Erro ao analisar: ${err.message}` }],
2086
- structuredContent: { ok: false, error: err.message }
2821
+ content: [{ type: "text", text: `Erro ao analisar: ${explainResult.error || "erro desconhecido"}` }],
2822
+ structuredContent: { ok: false, error: explainResult.error }
2087
2823
  };
2088
2824
  }
2825
+ const sc = explainResult.structuredContent;
2826
+ return {
2827
+ content: [{ type: "text", text: sc.formattedText }],
2828
+ structuredContent: {
2829
+ ok: true,
2830
+ oQueAconteceu: sc.oQueAconteceu,
2831
+ porQueProvavelmenteFalhou: sc.porQueProvavelmenteFalhou,
2832
+ oQueFazerAgora: sc.oQueFazerAgora,
2833
+ sugestaoCorrecao: sc.sugestaoCorrecao ?? null,
2834
+ conceito: sc.conceito ?? null,
2835
+ framework: sc.framework,
2836
+ formattedText: sc.formattedText
2837
+ }
2838
+ };
2089
2839
  }
2090
2840
  );
2091
2841
  server.registerTool(
@@ -2153,7 +2903,7 @@ server.registerTool(
2153
2903
  inputSchema: z.object({
2154
2904
  testFilePath: z.string().describe("Caminho do arquivo de teste que falhou (ex: specs/login.spec.js)."),
2155
2905
  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.")
2906
+ framework: z.enum(["cypress", "playwright", "webdriverio", "appium", "detox"]).optional().describe("Framework do teste. Detectado automaticamente se omitido.")
2157
2907
  }),
2158
2908
  outputSchema: z.object({
2159
2909
  ok: z.boolean(),
@@ -2168,9 +2918,9 @@ server.registerTool(
2168
2918
  const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
2169
2919
  let resolvedOutput = errorOutput;
2170
2920
  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");
2921
+ const logPath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
2922
+ if (fs6.existsSync(logPath)) {
2923
+ resolvedOutput = fs6.readFileSync(logPath, "utf8");
2174
2924
  }
2175
2925
  }
2176
2926
  if (!resolvedOutput) {
@@ -2186,10 +2936,10 @@ server.registerTool(
2186
2936
  };
2187
2937
  }
2188
2938
  let testCode = "";
2189
- const fullPath = path5.join(PROJECT_ROOT5, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
2190
- if (fs5.existsSync(fullPath)) {
2939
+ const fullPath = path6.join(PROJECT_ROOT6, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
2940
+ if (fs6.existsSync(fullPath)) {
2191
2941
  try {
2192
- testCode = fs5.readFileSync(fullPath, "utf8");
2942
+ testCode = fs6.readFileSync(fullPath, "utf8");
2193
2943
  } catch {
2194
2944
  }
2195
2945
  }
@@ -2205,15 +2955,18 @@ server.registerTool(
2205
2955
  cypress: "Cypress: cy.get('[data-testid=...]'), cy.contains(), cy.get('button').filter(':visible')",
2206
2956
  playwright: `Playwright: page.getByRole(), page.getByTestId(), page.locator('button:has-text("...")')`,
2207
2957
  webdriverio: "WebdriverIO: $('[data-testid=...]'), $('button=Texto')",
2208
- appium: "Appium: $('~accessibility-id'), $('//android.view.View')"
2958
+ 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.`,
2959
+ detox: `Detox: testID > accessibilityLabel > text. Explique por que \xE9 mais est\xE1vel.`
2209
2960
  };
2961
+ 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
2962
  const systemPrompt = `Voc\xEA \xE9 um especialista em testes E2E. O teste falhou porque um seletor n\xE3o encontrou o elemento (UI mudou).
2211
2963
  Analise o erro e o c\xF3digo e responda APENAS em JSON (sem markdown) com as chaves:
2212
2964
  - selectorSugerido: string (o novo seletor recomendado, mais resiliente)
2213
2965
  - 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)
2966
+ - 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
2967
 
2216
2968
  Priorize nesta ordem: data-testid > role + accessible name > texto vis\xEDvel > estrutura. Evite classes CSS e IDs que mudam.
2969
+ ${mobileRules}
2217
2970
 
2218
2971
  Framework: ${fw}. ${fwHints[fw] || ""}`;
2219
2972
  const userPrompt = `Output do erro:
@@ -2298,10 +3051,10 @@ server.registerTool(
2298
3051
  let instructions = "";
2299
3052
  let contextForGenerate = "";
2300
3053
  if (elementsJsonPath) {
2301
- const fullPath = path5.join(PROJECT_ROOT5, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
2302
- if (fs5.existsSync(fullPath)) {
3054
+ const fullPath = path6.join(PROJECT_ROOT6, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
3055
+ if (fs6.existsSync(fullPath)) {
2303
3056
  try {
2304
- const raw = fs5.readFileSync(fullPath, "utf8");
3057
+ const raw = fs6.readFileSync(fullPath, "utf8");
2305
3058
  const parsed = JSON.parse(raw);
2306
3059
  const arr = Array.isArray(parsed) ? parsed : parsed.elements || parsed.items || [];
2307
3060
  arr.forEach((el) => {
@@ -2402,20 +3155,20 @@ server.registerTool(
2402
3155
  },
2403
3156
  async ({ path: filePath }) => {
2404
3157
  const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
2405
- const fullPath = path5.join(PROJECT_ROOT5, normalized);
2406
- if (!fullPath.startsWith(PROJECT_ROOT5)) {
3158
+ const fullPath = path6.join(PROJECT_ROOT6, normalized);
3159
+ if (!fullPath.startsWith(PROJECT_ROOT6)) {
2407
3160
  return {
2408
3161
  content: [{ type: "text", text: "Caminho fora do projeto." }],
2409
3162
  structuredContent: { ok: false, error: "Path outside project" }
2410
3163
  };
2411
3164
  }
2412
- if (!fs5.existsSync(fullPath)) {
3165
+ if (!fs6.existsSync(fullPath)) {
2413
3166
  return {
2414
3167
  content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
2415
3168
  structuredContent: { ok: false, error: "File not found" }
2416
3169
  };
2417
3170
  }
2418
- const stat = fs5.statSync(fullPath);
3171
+ const stat = fs6.statSync(fullPath);
2419
3172
  if (stat.isDirectory()) {
2420
3173
  return {
2421
3174
  content: [{ type: "text", text: "\xC9 um diret\xF3rio. Informe um arquivo." }],
@@ -2424,7 +3177,7 @@ server.registerTool(
2424
3177
  }
2425
3178
  let fileContent = "";
2426
3179
  try {
2427
- fileContent = fs5.readFileSync(fullPath, "utf8");
3180
+ fileContent = fs6.readFileSync(fullPath, "utf8");
2428
3181
  } catch (err) {
2429
3182
  return {
2430
3183
  content: [{ type: "text", text: `Erro ao ler: ${err.message}` }],
@@ -2442,7 +3195,7 @@ server.registerTool(
2442
3195
  };
2443
3196
  }
2444
3197
  const { provider, apiKey, baseUrl, model } = llm;
2445
- const ext = path5.extname(fullPath).toLowerCase();
3198
+ const ext = path6.extname(fullPath).toLowerCase();
2446
3199
  const lang = [".ts", ".tsx"].includes(ext) ? "TypeScript" : [".js", ".jsx"].includes(ext) ? "JavaScript" : [".py"].includes(ext) ? "Python" : "c\xF3digo";
2447
3200
  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
3201
 
@@ -2634,9 +3387,9 @@ server.registerTool(
2634
3387
  const msByPeriod = { "7d": 7 * 24 * 60 * 60 * 1e3, "30d": 30 * 24 * 60 * 60 * 1e3, all: Infinity };
2635
3388
  const cutoff = now - msByPeriod[period];
2636
3389
  let data = { events: [] };
2637
- if (fs5.existsSync(METRICS_FILE2)) {
3390
+ if (fs6.existsSync(METRICS_FILE2)) {
2638
3391
  try {
2639
- data = JSON.parse(fs5.readFileSync(METRICS_FILE2, "utf8"));
3392
+ data = JSON.parse(fs6.readFileSync(METRICS_FILE2, "utf8"));
2640
3393
  } catch {
2641
3394
  }
2642
3395
  }
@@ -2673,9 +3426,9 @@ server.registerTool(
2673
3426
  };
2674
3427
  }
2675
3428
  let flowCoverage = null;
2676
- if (fs5.existsSync(FLOWS_CONFIG_FILE)) {
3429
+ if (fs6.existsSync(FLOWS_CONFIG_FILE)) {
2677
3430
  try {
2678
- const flowsConfig = JSON.parse(fs5.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
3431
+ const flowsConfig = JSON.parse(fs6.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
2679
3432
  const flows = flowsConfig.flows || [];
2680
3433
  const structure = detectProjectStructure();
2681
3434
  const allTestFiles = new Set(collectTestFiles(structure).map((e) => e.path));
@@ -2814,7 +3567,7 @@ server.registerTool(
2814
3567
  }
2815
3568
  return new Promise((resolve) => {
2816
3569
  const child = spawn2(cmd, args, {
2817
- cwd: PROJECT_ROOT5,
3570
+ cwd: PROJECT_ROOT6,
2818
3571
  stdio: ["inherit", "pipe", "pipe"],
2819
3572
  shell: process.platform === "win32",
2820
3573
  env: { ...process.env }
@@ -2860,13 +3613,13 @@ server.registerTool(
2860
3613
  async ({ packageManager = "auto" }) => {
2861
3614
  let pm = packageManager;
2862
3615
  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";
3616
+ if (fs6.existsSync(path6.join(PROJECT_ROOT6, "yarn.lock"))) pm = "yarn";
3617
+ else if (fs6.existsSync(path6.join(PROJECT_ROOT6, "pnpm-lock.yaml"))) pm = "pnpm";
2865
3618
  else pm = "npm";
2866
3619
  }
2867
3620
  return new Promise((resolve) => {
2868
3621
  const child = spawn2(pm, ["install"], {
2869
- cwd: PROJECT_ROOT5,
3622
+ cwd: PROJECT_ROOT6,
2870
3623
  stdio: "inherit",
2871
3624
  shell: process.platform === "win32",
2872
3625
  env: { ...process.env }
@@ -2921,9 +3674,9 @@ server.registerTool(
2921
3674
  report += `\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
2922
3675
  `;
2923
3676
  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));
3677
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3678
+ if (!fs6.existsSync(fullPath)) return [];
3679
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
2927
3680
  });
2928
3681
  report += `\u2705 ${testFiles.length} teste(s) encontrado(s)
2929
3682
 
@@ -2934,7 +3687,7 @@ server.registerTool(
2934
3687
  if (fw) {
2935
3688
  const runResult = await new Promise((resolve) => {
2936
3689
  const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run"], {
2937
- cwd: PROJECT_ROOT5,
3690
+ cwd: PROJECT_ROOT6,
2938
3691
  stdio: ["inherit", "pipe", "pipe"],
2939
3692
  shell: process.platform === "win32"
2940
3693
  });
@@ -3107,9 +3860,9 @@ server.registerTool(
3107
3860
  const memory = loadProjectMemory();
3108
3861
  const stats = getMemoryStats();
3109
3862
  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));
3863
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3864
+ if (!fs6.existsSync(fullPath)) return [];
3865
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3113
3866
  });
3114
3867
  let score = 0;
3115
3868
  const recommendations = [];
@@ -3176,9 +3929,9 @@ server.registerTool(
3176
3929
  const memory = loadProjectMemory();
3177
3930
  const suggestions = [];
3178
3931
  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());
3932
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
3933
+ if (!fs6.existsSync(fullPath)) return [];
3934
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => f.toLowerCase());
3182
3935
  });
3183
3936
  const criticalFlows = ["login", "logout", "checkout", "payment", "signup", "search"];
3184
3937
  const missingFlows = criticalFlows.filter((flow) => !testFiles.some((f) => f.includes(flow)));
@@ -3427,9 +4180,9 @@ server.registerTool(
3427
4180
  const structure = detectProjectStructure();
3428
4181
  const stats = getMemoryStats();
3429
4182
  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));
4183
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
4184
+ if (!fs6.existsSync(fullPath)) return [];
4185
+ return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
3433
4186
  });
3434
4187
  const industryBenchmarks = {
3435
4188
  coverageAvg: "70-80%",
@@ -3496,16 +4249,16 @@ server.registerTool(
3496
4249
  testFiles = [testFile];
3497
4250
  } else {
3498
4251
  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));
4252
+ const fullPath = path6.join(PROJECT_ROOT6, dir);
4253
+ if (!fs6.existsSync(fullPath)) return [];
4254
+ 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
4255
  });
3503
4256
  }
3504
4257
  const predictions = [];
3505
4258
  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");
4259
+ const fullPath = path6.join(PROJECT_ROOT6, file);
4260
+ if (!fs6.existsSync(fullPath)) continue;
4261
+ const content = fs6.readFileSync(fullPath, "utf8");
3509
4262
  const reasons = [];
3510
4263
  let riskScore = 0;
3511
4264
  if (/\.(class|id)\s*=|querySelector|\.class-name/i.test(content)) {
@@ -3580,7 +4333,7 @@ server.registerTool(
3580
4333
  if (fw === "jest") {
3581
4334
  return new Promise((resolve) => {
3582
4335
  const child = spawn2("npx", ["jest", "--coverage"], {
3583
- cwd: PROJECT_ROOT5,
4336
+ cwd: PROJECT_ROOT6,
3584
4337
  stdio: ["inherit", "pipe", "pipe"],
3585
4338
  shell: process.platform === "win32",
3586
4339
  env: { ...process.env }
@@ -3702,17 +4455,20 @@ server.registerTool(
3702
4455
  learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
3703
4456
  const { provider, apiKey, baseUrl, model } = llm;
3704
4457
  const memoryHints = memory.learnings?.filter((l) => l.fix).slice(-10).map((l) => l.fix).join("\n") || "";
4458
+ const packageInfo = structure.packageJson || {};
4459
+ const isESM = packageInfo.type === "module";
3705
4460
  const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
3706
4461
  ${UNIVERSAL_TEST_PRACTICES}
3707
4462
 
3708
4463
  ${memoryHints ? `Aprendizados anteriores (use como refer\xEAncia):
3709
4464
  ${memoryHints.slice(0, 1e3)}` : ""}
4465
+ ${isESM ? "\nIMPORTANTE: Use sintaxe ESM (import/export), N\xC3O use require()." : ""}
3710
4466
  Retorne SOMENTE o c\xF3digo, sem markdown.`;
3711
4467
  const userPrompt = `Contexto:
3712
4468
  ${contextLines}
3713
4469
 
3714
4470
  Gere teste para: ${request}
3715
- Framework: ${fw}`;
4471
+ Framework: ${fw}${isESM ? "\nUse import { test, expect } from '@playwright/test';" : ""}`;
3716
4472
  try {
3717
4473
  let specContent = "";
3718
4474
  if (provider === "gemini") {
@@ -3739,23 +4495,42 @@ Framework: ${fw}`;
3739
4495
  })
3740
4496
  });
3741
4497
  const data = await res.json();
4498
+ if (data.error) {
4499
+ learnings.push({ attempt, action: "llm_call", result: `\u274C API error: ${data.error.message}` });
4500
+ throw new Error(`API Error: ${data.error.message || JSON.stringify(data.error)}`);
4501
+ }
3742
4502
  specContent = data.choices?.[0]?.message?.content || "";
3743
4503
  }
4504
+ if (!specContent || specContent.trim().length === 0) {
4505
+ learnings.push({ attempt, action: "generate_test", result: "\u274C LLM retornou vazio" });
4506
+ throw new Error("LLM retornou conte\xFAdo vazio. Verifique sua API key e conex\xE3o.");
4507
+ }
4508
+ learnings.push({ attempt, action: "generate_test", result: `\u2705 recebido ${specContent.length} chars` });
3744
4509
  specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
3745
4510
  testContent = specContent;
4511
+ if (!testContent || testContent.trim().length === 0) {
4512
+ learnings.push({ attempt, action: "parse_code", result: "\u274C c\xF3digo vazio ap\xF3s parsing" });
4513
+ throw new Error("Ap\xF3s parsing, o c\xF3digo ficou vazio. Resposta do LLM pode estar em formato inesperado.");
4514
+ }
3746
4515
  if (!testFilePath) {
3747
4516
  const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
3748
4517
  const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
3749
4518
  const safeName = fileName + ext;
3750
- testFilePath = path5.join(baseDir, safeName);
3751
- if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
4519
+ testFilePath = path6.join(baseDir, safeName);
4520
+ if (!fs6.existsSync(baseDir)) fs6.mkdirSync(baseDir, { recursive: true });
3752
4521
  }
3753
- fs5.writeFileSync(testFilePath, testContent, "utf8");
3754
- learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
4522
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
4523
+ const writtenFileSize = fs6.statSync(testFilePath).size;
4524
+ if (writtenFileSize === 0) {
4525
+ learnings.push({ attempt, action: "write_test", result: "\u274C arquivo vazio ap\xF3s gravar" });
4526
+ throw new Error("Arquivo gravado mas est\xE1 vazio. Problema na escrita do arquivo.");
4527
+ }
4528
+ learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath} (${writtenFileSize} bytes)` });
3755
4529
  learnings.push({ attempt, action: "run_tests", result: "executando..." });
4530
+ const runArg = fw === "playwright" ? path6.relative(PROJECT_ROOT6, testFilePath).replace(/\\/g, "/") : testFilePath;
3756
4531
  const runResult = await new Promise((resolve) => {
3757
- const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
3758
- cwd: PROJECT_ROOT5,
4532
+ const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", runArg], {
4533
+ cwd: PROJECT_ROOT6,
3759
4534
  stdio: ["inherit", "pipe", "pipe"],
3760
4535
  shell: process.platform === "win32"
3761
4536
  });
@@ -3812,9 +4587,14 @@ ${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
3812
4587
  }
3813
4588
  learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
3814
4589
  const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
4590
+ if (!fixedCode || fixedCode.trim().length === 0) {
4591
+ learnings.push({ attempt, action: "apply_fix", result: "\u274C corre\xE7\xE3o vazia" });
4592
+ continue;
4593
+ }
3815
4594
  testContent = fixedCode;
3816
- fs5.writeFileSync(testFilePath, testContent, "utf8");
3817
- learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
4595
+ fs6.writeFileSync(testFilePath, testContent, "utf8");
4596
+ const fixedFileSize = fs6.statSync(testFilePath).size;
4597
+ learnings.push({ attempt, action: "apply_fix", result: `corre\xE7\xE3o aplicada (${fixedFileSize} bytes)` });
3818
4598
  if (flakyAnalysis.isLikelyFlaky) {
3819
4599
  const inferredPattern = inferFailurePattern(runResult.output, fw);
3820
4600
  const learningType = inferredPattern?.learningType || (flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix");
@@ -3898,15 +4678,15 @@ test.describe('${type.toUpperCase()} Test', () => {
3898
4678
  async function main() {
3899
4679
  const cmd = process.argv[2];
3900
4680
  if (cmd === "learning-hub") {
3901
- const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
3902
- const hubPath = path5.join(__dirname2, "..", "learning-hub", "src", "server.js");
4681
+ const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
4682
+ const hubPath = path6.join(__dirname2, "..", "learning-hub", "src", "server.js");
3903
4683
  const hubUrl2 = pathToFileURL(hubPath).href;
3904
4684
  await import(hubUrl2);
3905
4685
  return;
3906
4686
  }
3907
4687
  if (cmd === "slack-bot") {
3908
- const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
3909
- const slackBotPath = path5.join(__dirname2, "..", "slack-bot", "src", "index.js");
4688
+ const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
4689
+ const slackBotPath = path6.join(__dirname2, "..", "slack-bot", "src", "index.js");
3910
4690
  const slackBotUrl = pathToFileURL(slackBotPath).href;
3911
4691
  await import(slackBotUrl);
3912
4692
  return;