mcp-lab-agent 2.1.6 → 2.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.js +942 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/slack-bot/README.md +4 -1
- package/slack-bot/TROUBLESHOOTING.md +41 -5
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
|
|
10
|
-
import
|
|
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/
|
|
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
|
-
|
|
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 (
|
|
664
|
-
const raw =
|
|
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
|
-
|
|
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
|
|
719
|
-
import
|
|
862
|
+
import path5 from "path";
|
|
863
|
+
import fs5 from "fs";
|
|
720
864
|
import { spawn } from "child_process";
|
|
721
|
-
var
|
|
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] ?
|
|
881
|
+
const baseDir = structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : path5.join(PROJECT_ROOT5, "tests");
|
|
738
882
|
return { ext, baseDir };
|
|
739
883
|
}
|
|
740
884
|
async function handleCLI() {
|
|
@@ -754,6 +898,9 @@ COMANDOS CLI:
|
|
|
754
898
|
auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
|
|
755
899
|
stats Estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
|
|
756
900
|
report [--full] Relat\xF3rio de evolu\xE7\xE3o e aprendizado (--full = completo com recomenda\xE7\xF5es)
|
|
901
|
+
metrics-report [--json] [--output FILE] [path1 path2 ...] Relat\xF3rio de m\xE9tricas (m\xE9todo, resultado). Sem paths = projeto atual.
|
|
902
|
+
flaky-report [--runs N] [--spec FILE] [--output FILE] Detecta testes flaky: roda N vezes (default 3), identifica intermit\xEAncia e causa prov\xE1vel
|
|
903
|
+
run [spec] [--device NAME] [--no-auto-fix] Roda testes: detecta device, executa e aplica auto-fix de seletor se falhar
|
|
757
904
|
detect [--json] Detecta frameworks e estrutura
|
|
758
905
|
route <tarefa> Sugere qual ferramenta usar
|
|
759
906
|
list Lista ferramentas MCP dispon\xEDveis
|
|
@@ -765,6 +912,9 @@ EXEMPLOS:
|
|
|
765
912
|
mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
|
|
766
913
|
mcp-lab-agent auto "login flow" --max-retries 5
|
|
767
914
|
mcp-lab-agent stats
|
|
915
|
+
mcp-lab-agent flaky-report --runs 5 --output flaky.md
|
|
916
|
+
mcp-lab-agent run specs/login.spec.js
|
|
917
|
+
mcp-lab-agent run specs/login.spec.js --device iPhone_15
|
|
768
918
|
mcp-lab-agent detect --json
|
|
769
919
|
|
|
770
920
|
INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
|
|
@@ -897,8 +1047,465 @@ ${recommendations.map((r) => ` \u2022 ${r}`).join("\n")}` : ""}
|
|
|
897
1047
|
await handleAnalyzeCommand();
|
|
898
1048
|
return true;
|
|
899
1049
|
}
|
|
1050
|
+
if (cmd === "metrics-report") {
|
|
1051
|
+
await handleMetricsReportCommand();
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
if (cmd === "flaky-report") {
|
|
1055
|
+
await handleFlakyReportCommand();
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
if (cmd === "run") {
|
|
1059
|
+
await handleRunCommand();
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
900
1062
|
return false;
|
|
901
1063
|
}
|
|
1064
|
+
async function handleMetricsReportCommand() {
|
|
1065
|
+
const argv = process.argv.slice(2);
|
|
1066
|
+
const jsonOnly = argv.includes("--json");
|
|
1067
|
+
const outputIdx = argv.indexOf("--output");
|
|
1068
|
+
const outputFile = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : null;
|
|
1069
|
+
const paths = argv.filter((a) => {
|
|
1070
|
+
if (a.startsWith("--") || a === "metrics-report") return false;
|
|
1071
|
+
if (outputIdx !== -1 && a === argv[outputIdx + 1]) return false;
|
|
1072
|
+
return true;
|
|
1073
|
+
});
|
|
1074
|
+
const projectDirs = paths.length > 0 ? paths : [PROJECT_ROOT5];
|
|
1075
|
+
const reports = [];
|
|
1076
|
+
for (const dir of projectDirs) {
|
|
1077
|
+
const resolved = path5.resolve(dir);
|
|
1078
|
+
if (!fs5.existsSync(resolved)) {
|
|
1079
|
+
console.warn(`\u26A0\uFE0F Diret\xF3rio n\xE3o encontrado: ${dir}`);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const memoryPath = path5.join(resolved, ".qa-lab-memory.json");
|
|
1083
|
+
const metricsPath = path5.join(resolved, ".qa-lab-metrics.json");
|
|
1084
|
+
let memory = {};
|
|
1085
|
+
let metrics = { events: [] };
|
|
1086
|
+
if (fs5.existsSync(memoryPath)) {
|
|
1087
|
+
try {
|
|
1088
|
+
memory = JSON.parse(fs5.readFileSync(memoryPath, "utf8"));
|
|
1089
|
+
} catch {
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (fs5.existsSync(metricsPath)) {
|
|
1093
|
+
try {
|
|
1094
|
+
metrics = JSON.parse(fs5.readFileSync(metricsPath, "utf8"));
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const events = metrics.events || [];
|
|
1099
|
+
const executions = memory.executions || [];
|
|
1100
|
+
const learnings = memory.learnings || [];
|
|
1101
|
+
const byEventType = {};
|
|
1102
|
+
events.forEach((e) => {
|
|
1103
|
+
const t = e.type || "unknown";
|
|
1104
|
+
byEventType[t] = (byEventType[t] || 0) + 1;
|
|
1105
|
+
});
|
|
1106
|
+
const testRuns = events.filter((e) => e.type === "test_run");
|
|
1107
|
+
const testRunPassed = testRuns.filter((e) => e.exitCode === 0).length;
|
|
1108
|
+
const testRunFailed = testRuns.filter((e) => e.exitCode !== 0).length;
|
|
1109
|
+
const byFramework = {};
|
|
1110
|
+
testRuns.forEach((e) => {
|
|
1111
|
+
const f = e.framework || "unknown";
|
|
1112
|
+
byFramework[f] = byFramework[f] || { total: 0, passed: 0, failed: 0 };
|
|
1113
|
+
byFramework[f].total++;
|
|
1114
|
+
if (e.exitCode === 0) byFramework[f].passed++;
|
|
1115
|
+
else byFramework[f].failed++;
|
|
1116
|
+
});
|
|
1117
|
+
const byLearningType = {};
|
|
1118
|
+
learnings.forEach((l) => {
|
|
1119
|
+
const t = l.type || "unknown";
|
|
1120
|
+
byLearningType[t] = byLearningType[t] || { total: 0, success: 0 };
|
|
1121
|
+
byLearningType[t].total++;
|
|
1122
|
+
if (l.success) byLearningType[t].success++;
|
|
1123
|
+
});
|
|
1124
|
+
const execByFramework = {};
|
|
1125
|
+
executions.forEach((e) => {
|
|
1126
|
+
const f = e.framework || "unknown";
|
|
1127
|
+
execByFramework[f] = execByFramework[f] || { total: 0, passed: 0 };
|
|
1128
|
+
execByFramework[f].total++;
|
|
1129
|
+
if (e.passed) execByFramework[f].passed++;
|
|
1130
|
+
});
|
|
1131
|
+
const projectName = path5.basename(resolved);
|
|
1132
|
+
reports.push({
|
|
1133
|
+
project: projectName,
|
|
1134
|
+
path: resolved,
|
|
1135
|
+
summary: {
|
|
1136
|
+
eventsTotal: events.length,
|
|
1137
|
+
eventTypes: byEventType,
|
|
1138
|
+
testRuns: { total: testRuns.length, passed: testRunPassed, failed: testRunFailed },
|
|
1139
|
+
byFramework,
|
|
1140
|
+
executions: { total: executions.length, byFramework: execByFramework },
|
|
1141
|
+
learnings: { total: learnings.length, byType: byLearningType },
|
|
1142
|
+
lastUpdated: metrics.lastUpdated || memory.updatedAt
|
|
1143
|
+
},
|
|
1144
|
+
recentEvents: events.slice(-20).map((e) => ({
|
|
1145
|
+
type: e.type,
|
|
1146
|
+
timestamp: e.timestamp,
|
|
1147
|
+
framework: e.framework,
|
|
1148
|
+
spec: e.spec,
|
|
1149
|
+
passed: e.passed,
|
|
1150
|
+
failed: e.failed,
|
|
1151
|
+
exitCode: e.exitCode,
|
|
1152
|
+
durationSeconds: e.durationSeconds
|
|
1153
|
+
}))
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
if (jsonOnly) {
|
|
1157
|
+
const out = JSON.stringify({ projects: reports, generatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
1158
|
+
if (outputFile) fs5.writeFileSync(outputFile, out, "utf8");
|
|
1159
|
+
else console.log(out);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
let report = `# Relat\xF3rio de M\xE9tricas \u2014 mcp-lab-agent
|
|
1163
|
+
|
|
1164
|
+
`;
|
|
1165
|
+
report += `Gerado em: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1166
|
+
`;
|
|
1167
|
+
report += `Projetos: ${reports.length}
|
|
1168
|
+
|
|
1169
|
+
`;
|
|
1170
|
+
report += `---
|
|
1171
|
+
|
|
1172
|
+
`;
|
|
1173
|
+
for (const r of reports) {
|
|
1174
|
+
report += `## ${r.project}
|
|
1175
|
+
|
|
1176
|
+
`;
|
|
1177
|
+
report += `**Caminho:** \`${r.path}\`
|
|
1178
|
+
|
|
1179
|
+
`;
|
|
1180
|
+
const s = r.summary;
|
|
1181
|
+
report += `### Eventos (.qa-lab-metrics.json)
|
|
1182
|
+
|
|
1183
|
+
`;
|
|
1184
|
+
report += `| M\xE9todo/Tipo | Total | Descri\xE7\xE3o |
|
|
1185
|
+
`;
|
|
1186
|
+
report += `|-------------|-------|
|
|
1187
|
+
`;
|
|
1188
|
+
for (const [t, count] of Object.entries(s.eventTypes || {})) {
|
|
1189
|
+
let desc = "";
|
|
1190
|
+
if (t === "test_run") desc = `Execu\xE7\xE3o de testes (passed/failed por framework abaixo)`;
|
|
1191
|
+
else if (t === "bug_reported") desc = "Bug report gerado";
|
|
1192
|
+
else desc = t;
|
|
1193
|
+
report += `| ${t} | ${count} | ${desc} |
|
|
1194
|
+
`;
|
|
1195
|
+
}
|
|
1196
|
+
if (Object.keys(s.eventTypes || {}).length === 0) {
|
|
1197
|
+
report += `| \u2014 | 0 | Nenhum evento registrado |
|
|
1198
|
+
`;
|
|
1199
|
+
}
|
|
1200
|
+
report += `
|
|
1201
|
+
`;
|
|
1202
|
+
if (s.testRuns?.total > 0) {
|
|
1203
|
+
report += `### Resultado de Execu\xE7\xF5es (run_tests)
|
|
1204
|
+
|
|
1205
|
+
`;
|
|
1206
|
+
report += `| Framework | Total | Passed | Failed | Taxa sucesso |
|
|
1207
|
+
`;
|
|
1208
|
+
report += `|-----------|-------|--------|--------|---------------|
|
|
1209
|
+
`;
|
|
1210
|
+
for (const [fw, data] of Object.entries(s.byFramework || {})) {
|
|
1211
|
+
const rate = data.total > 0 ? Math.round(data.passed / data.total * 100) : 0;
|
|
1212
|
+
report += `| ${fw} | ${data.total} | ${data.passed} | ${data.failed} | ${rate}% |
|
|
1213
|
+
`;
|
|
1214
|
+
}
|
|
1215
|
+
report += `
|
|
1216
|
+
`;
|
|
1217
|
+
report += `**Resumo:** ${s.testRuns.passed} passed, ${s.testRuns.failed} failed (total: ${s.testRuns.total})
|
|
1218
|
+
|
|
1219
|
+
`;
|
|
1220
|
+
}
|
|
1221
|
+
if (s.executions?.total > 0) {
|
|
1222
|
+
report += `### Hist\xF3rico de Execu\xE7\xF5es (memory)
|
|
1223
|
+
|
|
1224
|
+
`;
|
|
1225
|
+
report += `| Framework | Total | Passed | Taxa |
|
|
1226
|
+
`;
|
|
1227
|
+
report += `|-----------|-------|--------|------|
|
|
1228
|
+
`;
|
|
1229
|
+
for (const [fw, data] of Object.entries(s.executions.byFramework || {})) {
|
|
1230
|
+
const rate = data.total > 0 ? Math.round(data.passed / data.total * 100) : 0;
|
|
1231
|
+
report += `| ${fw} | ${data.total} | ${data.passed} | ${rate}% |
|
|
1232
|
+
`;
|
|
1233
|
+
}
|
|
1234
|
+
report += `
|
|
1235
|
+
`;
|
|
1236
|
+
}
|
|
1237
|
+
if (s.learnings?.total > 0) {
|
|
1238
|
+
report += `### Aprendizados (.qa-lab-memory.json)
|
|
1239
|
+
|
|
1240
|
+
`;
|
|
1241
|
+
report += `| Tipo | Total | Sucesso | Taxa |
|
|
1242
|
+
`;
|
|
1243
|
+
report += `|------|-------|---------|------|
|
|
1244
|
+
`;
|
|
1245
|
+
for (const [t, data] of Object.entries(s.learnings.byType || {})) {
|
|
1246
|
+
const rate = data.total > 0 ? Math.round(data.success / data.total * 100) : 0;
|
|
1247
|
+
report += `| ${t} | ${data.total} | ${data.success} | ${rate}% |
|
|
1248
|
+
`;
|
|
1249
|
+
}
|
|
1250
|
+
report += `
|
|
1251
|
+
`;
|
|
1252
|
+
}
|
|
1253
|
+
if (r.recentEvents?.length > 0) {
|
|
1254
|
+
report += `### \xDAltimos 20 eventos
|
|
1255
|
+
|
|
1256
|
+
`;
|
|
1257
|
+
report += `| Data | Tipo | Framework | Spec | Passed | Failed | Exit | Dura\xE7\xE3o(s) |
|
|
1258
|
+
`;
|
|
1259
|
+
report += `|------|------|-----------|------|--------|--------|------|------------|
|
|
1260
|
+
`;
|
|
1261
|
+
for (const e of r.recentEvents.slice(-10)) {
|
|
1262
|
+
const ts = e.timestamp ? new Date(e.timestamp).toLocaleString() : "\u2014";
|
|
1263
|
+
report += `| ${ts} | ${e.type || "\u2014"} | ${e.framework || "\u2014"} | ${(e.spec || "\u2014").slice(0, 20)} | ${e.passed ?? "\u2014"} | ${e.failed ?? "\u2014"} | ${e.exitCode ?? "\u2014"} | ${e.durationSeconds ?? "\u2014"} |
|
|
1264
|
+
`;
|
|
1265
|
+
}
|
|
1266
|
+
report += `
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
if (s.lastUpdated) {
|
|
1270
|
+
report += `*\xDAltima atualiza\xE7\xE3o: ${s.lastUpdated}*
|
|
1271
|
+
|
|
1272
|
+
`;
|
|
1273
|
+
}
|
|
1274
|
+
report += `---
|
|
1275
|
+
|
|
1276
|
+
`;
|
|
1277
|
+
}
|
|
1278
|
+
if (outputFile) {
|
|
1279
|
+
fs5.writeFileSync(outputFile, report, "utf8");
|
|
1280
|
+
console.log(`
|
|
1281
|
+
\u{1F4C4} Relat\xF3rio salvo em: ${outputFile}
|
|
1282
|
+
`);
|
|
1283
|
+
} else {
|
|
1284
|
+
console.log(report);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async function handleFlakyReportCommand() {
|
|
1288
|
+
const argv = process.argv.slice(2);
|
|
1289
|
+
const runsIdx = argv.indexOf("--runs");
|
|
1290
|
+
const runs = runsIdx !== -1 && argv[runsIdx + 1] ? parseInt(argv[runsIdx + 1], 10) : 3;
|
|
1291
|
+
const specIdx = argv.indexOf("--spec");
|
|
1292
|
+
const spec = specIdx !== -1 && argv[specIdx + 1] ? argv[specIdx + 1] : null;
|
|
1293
|
+
const outputIdx = argv.indexOf("--output");
|
|
1294
|
+
const outputFile = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : null;
|
|
1295
|
+
const structure = detectProjectStructure();
|
|
1296
|
+
if (!structure.hasTests) {
|
|
1297
|
+
console.error("\u274C Nenhum framework de teste detectado.");
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
const fw = structure.testFrameworks[0];
|
|
1301
|
+
const { cmd, args, cwd } = getRunCommand(structure, fw, spec);
|
|
1302
|
+
console.log(`
|
|
1303
|
+
\u{1F52C} Relat\xF3rio de testes flaky
|
|
1304
|
+
`);
|
|
1305
|
+
console.log(`Framework: ${fw}`);
|
|
1306
|
+
console.log(`Execu\xE7\xF5es: ${runs}`);
|
|
1307
|
+
if (spec) console.log(`Spec: ${spec}`);
|
|
1308
|
+
console.log(`
|
|
1309
|
+
Rodando testes ${runs}x...
|
|
1310
|
+
`);
|
|
1311
|
+
const results = [];
|
|
1312
|
+
for (let i = 0; i < runs; i++) {
|
|
1313
|
+
process.stdout.write(` [${i + 1}/${runs}] `);
|
|
1314
|
+
const result = await runTestsOnce(cmd, args, cwd);
|
|
1315
|
+
results.push(result);
|
|
1316
|
+
process.stdout.write(result.passed ? "\u2705 passou\n" : "\u274C falhou\n");
|
|
1317
|
+
}
|
|
1318
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1319
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
1320
|
+
const isFlaky = passed > 0 && failed > 0;
|
|
1321
|
+
const failureOutput = results.find((r) => !r.passed)?.output || "";
|
|
1322
|
+
const flakyAnalysis = failureOutput ? detectFlakyPatterns(failureOutput) : null;
|
|
1323
|
+
const probableCause = flakyAnalysis?.isLikelyFlaky ? flakyAnalysis.patterns.map((p) => `${p.pattern}: ${p.suggestion}`).join("; ") : "N\xE3o foi poss\xEDvel inferir (rode com explainOnFailure para an\xE1lise detalhada)";
|
|
1324
|
+
let report = `# Relat\xF3rio de testes flaky \u2014 mcp-lab-agent
|
|
1325
|
+
|
|
1326
|
+
`;
|
|
1327
|
+
report += `Gerado em: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1328
|
+
`;
|
|
1329
|
+
report += `Framework: ${fw}
|
|
1330
|
+
`;
|
|
1331
|
+
report += `Execu\xE7\xF5es: ${runs}
|
|
1332
|
+
`;
|
|
1333
|
+
if (spec) report += `Spec: ${spec}
|
|
1334
|
+
`;
|
|
1335
|
+
report += `
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
`;
|
|
1339
|
+
report += `## Resultado
|
|
1340
|
+
|
|
1341
|
+
`;
|
|
1342
|
+
report += `| M\xE9trica | Valor |
|
|
1343
|
+
`;
|
|
1344
|
+
report += `|---------|-------|
|
|
1345
|
+
`;
|
|
1346
|
+
report += `| Passou | ${passed}/${runs} |
|
|
1347
|
+
`;
|
|
1348
|
+
report += `| Falhou | ${failed}/${runs} |
|
|
1349
|
+
`;
|
|
1350
|
+
report += `| Taxa de falha | ${Math.round(failed / runs * 100)}% |
|
|
1351
|
+
`;
|
|
1352
|
+
report += `| **Flaky?** | ${isFlaky ? "\u26A0\uFE0F SIM" : failed === runs ? "\u274C Falha consistente" : "\u2705 Est\xE1vel"} |
|
|
1353
|
+
|
|
1354
|
+
`;
|
|
1355
|
+
if (isFlaky) {
|
|
1356
|
+
report += `## Causa prov\xE1vel
|
|
1357
|
+
|
|
1358
|
+
`;
|
|
1359
|
+
report += `${probableCause}
|
|
1360
|
+
|
|
1361
|
+
`;
|
|
1362
|
+
if (flakyAnalysis?.patterns?.length) {
|
|
1363
|
+
report += `### Sugest\xF5es
|
|
1364
|
+
|
|
1365
|
+
`;
|
|
1366
|
+
flakyAnalysis.patterns.forEach((p) => {
|
|
1367
|
+
report += `- **${p.pattern}:** ${p.suggestion}
|
|
1368
|
+
`;
|
|
1369
|
+
});
|
|
1370
|
+
report += `
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (failed > 0 && failureOutput) {
|
|
1375
|
+
report += `## \xDAltima sa\xEDda de falha (trecho)
|
|
1376
|
+
|
|
1377
|
+
`;
|
|
1378
|
+
report += "```\n";
|
|
1379
|
+
report += failureOutput.slice(0, 1500).trim();
|
|
1380
|
+
if (failureOutput.length > 1500) report += "\n...";
|
|
1381
|
+
report += "\n```\n\n";
|
|
1382
|
+
}
|
|
1383
|
+
report += `---
|
|
1384
|
+
|
|
1385
|
+
`;
|
|
1386
|
+
report += `*Use \`mcp-lab-agent por_que_falhou\` (via MCP) ou \`run_tests\` com \`explainOnFailure: true\` para an\xE1lise detalhada.*
|
|
1387
|
+
`;
|
|
1388
|
+
if (outputFile) {
|
|
1389
|
+
fs5.writeFileSync(outputFile, report, "utf8");
|
|
1390
|
+
console.log(`
|
|
1391
|
+
\u{1F4C4} Relat\xF3rio salvo em: ${outputFile}
|
|
1392
|
+
`);
|
|
1393
|
+
} else {
|
|
1394
|
+
console.log("\n" + report);
|
|
1395
|
+
}
|
|
1396
|
+
process.exit(isFlaky ? 1 : 0);
|
|
1397
|
+
}
|
|
1398
|
+
function getRunCommand(structure, fw, spec) {
|
|
1399
|
+
const cwdMap = {
|
|
1400
|
+
cypress: structure.testDirs.includes("cypress") ? path5.join(PROJECT_ROOT5, "cypress") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5,
|
|
1401
|
+
playwright: structure.testDirs.includes("playwright") ? path5.join(PROJECT_ROOT5, "playwright") : structure.testDirs[0] ? path5.join(PROJECT_ROOT5, structure.testDirs[0]) : PROJECT_ROOT5
|
|
1402
|
+
};
|
|
1403
|
+
const cwd = cwdMap[fw] || getFrameworkCwd(structure, ["specs", "tests", "e2e"]) || PROJECT_ROOT5;
|
|
1404
|
+
if (fw === "cypress") {
|
|
1405
|
+
return { cmd: "npx", args: spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"], cwd };
|
|
1406
|
+
}
|
|
1407
|
+
if (fw === "playwright") {
|
|
1408
|
+
return { cmd: "npx", args: spec ? ["playwright", "test", spec] : ["playwright", "test"], cwd };
|
|
1409
|
+
}
|
|
1410
|
+
if (fw === "webdriverio" || fw === "appium") {
|
|
1411
|
+
return { cmd: "npx", args: spec ? ["wdio", "run", spec] : ["wdio", "run"], cwd: PROJECT_ROOT5 };
|
|
1412
|
+
}
|
|
1413
|
+
if (fw === "jest") {
|
|
1414
|
+
return { cmd: "npx", args: spec ? ["jest", spec] : ["jest"], cwd: PROJECT_ROOT5 };
|
|
1415
|
+
}
|
|
1416
|
+
if (fw === "vitest") {
|
|
1417
|
+
return { cmd: "npx", args: ["vitest", "run", ...spec ? [spec] : []], cwd: PROJECT_ROOT5 };
|
|
1418
|
+
}
|
|
1419
|
+
if (fw === "mocha") {
|
|
1420
|
+
return { cmd: "npx", args: spec ? ["mocha", spec] : ["mocha"], cwd: PROJECT_ROOT5 };
|
|
1421
|
+
}
|
|
1422
|
+
if (fw === "pytest") {
|
|
1423
|
+
return { cmd: "pytest", args: spec ? [spec] : [], cwd: PROJECT_ROOT5 };
|
|
1424
|
+
}
|
|
1425
|
+
if (fw === "robot") {
|
|
1426
|
+
return { cmd: "robot", args: spec ? [spec] : [structure.testDirs[0] || "tests"], cwd: PROJECT_ROOT5 };
|
|
1427
|
+
}
|
|
1428
|
+
return { cmd: "npm", args: ["test"], cwd: PROJECT_ROOT5 };
|
|
1429
|
+
}
|
|
1430
|
+
async function handleRunCommand() {
|
|
1431
|
+
const argv = process.argv.slice(2);
|
|
1432
|
+
const deviceIdx = argv.indexOf("--device");
|
|
1433
|
+
const device = deviceIdx !== -1 && argv[deviceIdx + 1] ? argv[deviceIdx + 1] : null;
|
|
1434
|
+
const noAutoFix = argv.includes("--no-auto-fix");
|
|
1435
|
+
const spec = argv.filter((a) => !a.startsWith("--") && a !== "run")[0] || null;
|
|
1436
|
+
const structure = detectProjectStructure();
|
|
1437
|
+
if (!structure.hasTests) {
|
|
1438
|
+
console.error("\u274C Nenhum framework de teste detectado.");
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
const fw = structure.testFrameworks[0];
|
|
1442
|
+
const deviceConfig = structure.hasMobile ? detectDeviceConfig(structure) : {};
|
|
1443
|
+
const useDevice = device || deviceConfig.configuration || deviceConfig.device;
|
|
1444
|
+
const doAutoFix = !noAutoFix && structure.hasMobile && !!spec;
|
|
1445
|
+
let runEnv = { ...process.env };
|
|
1446
|
+
if (Object.keys(deviceConfig.envOverrides || {}).length) {
|
|
1447
|
+
runEnv = { ...runEnv, ...deviceConfig.envOverrides };
|
|
1448
|
+
}
|
|
1449
|
+
if (device) {
|
|
1450
|
+
if (fw === "detox") runEnv.DETOX_CONFIGURATION = device;
|
|
1451
|
+
else if (fw === "appium") runEnv.APPIUM_DEVICE_NAME = device;
|
|
1452
|
+
} else if (deviceConfig.configuration && fw === "detox") {
|
|
1453
|
+
runEnv.DETOX_CONFIGURATION = deviceConfig.configuration;
|
|
1454
|
+
}
|
|
1455
|
+
let { cmd, args, cwd } = getRunCommand(structure, fw, spec);
|
|
1456
|
+
if (fw === "detox" && useDevice) {
|
|
1457
|
+
args = [...args.slice(0, 2), "--configuration", useDevice, ...args.slice(2)];
|
|
1458
|
+
}
|
|
1459
|
+
const isSelectorFailure = (out) => /element not found|selector|timeout|locator|cy\.get|page\.locator|Unable to find/i.test(out || "");
|
|
1460
|
+
console.log(`
|
|
1461
|
+
\u25B6\uFE0F Rodando testes${spec ? `: ${spec}` : ""}
|
|
1462
|
+
`);
|
|
1463
|
+
if (useDevice) console.log(` Device: ${useDevice}
|
|
1464
|
+
`);
|
|
1465
|
+
let result = await runTestsOnce(cmd, args, cwd, runEnv);
|
|
1466
|
+
let autoFixed = false;
|
|
1467
|
+
if (!result.passed && doAutoFix && isSelectorFailure(result.runOutput) && resolveLLMProvider("complex").apiKey) {
|
|
1468
|
+
console.log("\n\u26A0\uFE0F Falha por seletor. Aplicando corre\xE7\xE3o autom\xE1tica...\n");
|
|
1469
|
+
const fixResult = await applySelectorFixAndRetry(spec, result.runOutput, fw);
|
|
1470
|
+
if (fixResult.applied) {
|
|
1471
|
+
autoFixed = true;
|
|
1472
|
+
result = await runTestsOnce(cmd, args, cwd, runEnv);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (result.passed) {
|
|
1476
|
+
console.log(`
|
|
1477
|
+
\u2705 Testes passaram${autoFixed ? " (ap\xF3s corre\xE7\xE3o de seletor)" : ""}.
|
|
1478
|
+
`);
|
|
1479
|
+
} else {
|
|
1480
|
+
console.log(`
|
|
1481
|
+
\u274C Testes falharam.
|
|
1482
|
+
`);
|
|
1483
|
+
if (result.runOutput) console.log(result.runOutput.slice(0, 800) + (result.runOutput.length > 800 ? "\n..." : ""));
|
|
1484
|
+
}
|
|
1485
|
+
process.exit(result.passed ? 0 : 1);
|
|
1486
|
+
}
|
|
1487
|
+
function runTestsOnce(cmd, args, cwd, env = process.env) {
|
|
1488
|
+
return new Promise((resolve) => {
|
|
1489
|
+
const child = spawn(cmd, args, {
|
|
1490
|
+
cwd,
|
|
1491
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1492
|
+
shell: process.platform === "win32",
|
|
1493
|
+
env: { ...process.env, ...env }
|
|
1494
|
+
});
|
|
1495
|
+
let stdout = "";
|
|
1496
|
+
let stderr = "";
|
|
1497
|
+
if (child.stdout) child.stdout.on("data", (d) => {
|
|
1498
|
+
stdout += d.toString();
|
|
1499
|
+
});
|
|
1500
|
+
if (child.stderr) child.stderr.on("data", (d) => {
|
|
1501
|
+
stderr += d.toString();
|
|
1502
|
+
});
|
|
1503
|
+
child.on("close", (code) => {
|
|
1504
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
1505
|
+
resolve({ passed: code === 0, code: code ?? 1, output });
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
902
1509
|
async function handleAutoCommand() {
|
|
903
1510
|
const request = process.argv.slice(3).join(" ");
|
|
904
1511
|
if (!request) {
|
|
@@ -979,16 +1586,16 @@ Framework: ${fw}`;
|
|
|
979
1586
|
const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
|
|
980
1587
|
const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
|
|
981
1588
|
const safeName = fileName + ext;
|
|
982
|
-
testFilePath =
|
|
983
|
-
if (!
|
|
1589
|
+
testFilePath = path5.join(baseDir, safeName);
|
|
1590
|
+
if (!fs5.existsSync(baseDir)) fs5.mkdirSync(baseDir, { recursive: true });
|
|
984
1591
|
}
|
|
985
|
-
|
|
1592
|
+
fs5.writeFileSync(testFilePath, testContent, "utf8");
|
|
986
1593
|
console.log(`\u2705 Teste gravado: ${testFilePath}`);
|
|
987
1594
|
console.log(`
|
|
988
1595
|
[Tentativa ${attempt}/${maxRetries}] Executando teste...`);
|
|
989
1596
|
const runResult = await new Promise((resolve) => {
|
|
990
1597
|
const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
|
|
991
|
-
cwd:
|
|
1598
|
+
cwd: PROJECT_ROOT5,
|
|
992
1599
|
stdio: ["inherit", "pipe", "pipe"],
|
|
993
1600
|
shell: process.platform === "win32"
|
|
994
1601
|
});
|
|
@@ -1055,9 +1662,9 @@ async function handleAnalyzeCommand() {
|
|
|
1055
1662
|
console.log(`\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
|
|
1056
1663
|
`);
|
|
1057
1664
|
const testFiles = structure.testDirs.flatMap((dir) => {
|
|
1058
|
-
const fullPath =
|
|
1059
|
-
if (!
|
|
1060
|
-
return
|
|
1665
|
+
const fullPath = path5.join(PROJECT_ROOT5, dir);
|
|
1666
|
+
if (!fs5.existsSync(fullPath)) return [];
|
|
1667
|
+
return fs5.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
|
|
1061
1668
|
});
|
|
1062
1669
|
console.log(`\u2705 ${testFiles.length} teste(s) encontrado(s)
|
|
1063
1670
|
`);
|
|
@@ -1116,13 +1723,13 @@ async function handleAnalyzeCommand() {
|
|
|
1116
1723
|
}
|
|
1117
1724
|
|
|
1118
1725
|
// src/index.js
|
|
1119
|
-
var
|
|
1120
|
-
config({ path:
|
|
1726
|
+
var PROJECT_ROOT6 = process.cwd();
|
|
1727
|
+
config({ path: path6.join(PROJECT_ROOT6, ".env") });
|
|
1121
1728
|
var server = new McpServer({
|
|
1122
1729
|
name: "mcp-lab-agent",
|
|
1123
|
-
version: "2.1.
|
|
1730
|
+
version: "2.1.9"
|
|
1124
1731
|
});
|
|
1125
|
-
var METRICS_FILE2 =
|
|
1732
|
+
var METRICS_FILE2 = path6.join(PROJECT_ROOT6, ".qa-lab-metrics.json");
|
|
1126
1733
|
function appendMetricsEvent(event) {
|
|
1127
1734
|
recordMetricEvent(event);
|
|
1128
1735
|
}
|
|
@@ -1143,20 +1750,20 @@ server.registerTool(
|
|
|
1143
1750
|
},
|
|
1144
1751
|
async ({ path: filePath, encoding = "utf8" }) => {
|
|
1145
1752
|
const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
|
|
1146
|
-
const fullPath =
|
|
1147
|
-
if (!fullPath.startsWith(
|
|
1753
|
+
const fullPath = path6.join(PROJECT_ROOT6, normalized);
|
|
1754
|
+
if (!fullPath.startsWith(PROJECT_ROOT6)) {
|
|
1148
1755
|
return {
|
|
1149
1756
|
content: [{ type: "text", text: "Caminho fora do projeto." }],
|
|
1150
1757
|
structuredContent: { ok: false, error: "Path outside project" }
|
|
1151
1758
|
};
|
|
1152
1759
|
}
|
|
1153
|
-
if (!
|
|
1760
|
+
if (!fs6.existsSync(fullPath)) {
|
|
1154
1761
|
return {
|
|
1155
1762
|
content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
|
|
1156
1763
|
structuredContent: { ok: false, error: "File not found" }
|
|
1157
1764
|
};
|
|
1158
1765
|
}
|
|
1159
|
-
const stat =
|
|
1766
|
+
const stat = fs6.statSync(fullPath);
|
|
1160
1767
|
if (stat.isDirectory()) {
|
|
1161
1768
|
return {
|
|
1162
1769
|
content: [{ type: "text", text: "\xC9 um diret\xF3rio. Use um caminho de arquivo." }],
|
|
@@ -1164,7 +1771,7 @@ server.registerTool(
|
|
|
1164
1771
|
};
|
|
1165
1772
|
}
|
|
1166
1773
|
try {
|
|
1167
|
-
const content =
|
|
1774
|
+
const content = fs6.readFileSync(fullPath, encoding);
|
|
1168
1775
|
return {
|
|
1169
1776
|
content: [{ type: "text", text: content }],
|
|
1170
1777
|
structuredContent: { ok: true, content }
|
|
@@ -1248,7 +1855,7 @@ server.registerTool(
|
|
|
1248
1855
|
structuredContent: { ok: false, error: "Playwright not installed. Run: npm install playwright" }
|
|
1249
1856
|
};
|
|
1250
1857
|
}
|
|
1251
|
-
const outPath = screenshotPath ?
|
|
1858
|
+
const outPath = screenshotPath ? path6.join(PROJECT_ROOT6, screenshotPath.replace(/^\//, "")) : path6.join(PROJECT_ROOT6, ".qa-lab-screenshot.png");
|
|
1252
1859
|
const consoleLogs = [];
|
|
1253
1860
|
const consoleErrors = [];
|
|
1254
1861
|
const networkRequests = [];
|
|
@@ -1275,7 +1882,7 @@ server.registerTool(
|
|
|
1275
1882
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
1276
1883
|
await page.screenshot({ path: outPath, fullPage: false });
|
|
1277
1884
|
await browser.close();
|
|
1278
|
-
const relPath =
|
|
1885
|
+
const relPath = path6.relative(PROJECT_ROOT6, outPath);
|
|
1279
1886
|
let summary = `Screenshot salvo: ${relPath}`;
|
|
1280
1887
|
if (consoleErrors.length) summary += `
|
|
1281
1888
|
|
|
@@ -1397,7 +2004,9 @@ server.registerTool(
|
|
|
1397
2004
|
]).optional().describe("Framework espec\xEDfico ou 'npm' para npm test."),
|
|
1398
2005
|
spec: z.string().optional().describe("Caminho do spec (ex: cypress/e2e/test.cy.js)."),
|
|
1399
2006
|
suite: z.string().optional().describe("Suite ou pattern (ex: e2e, api)."),
|
|
1400
|
-
|
|
2007
|
+
device: z.string().optional().describe("Device/configuration para mobile. Se vazio, detecta de qa-lab-agent.config.json, wdio.conf ou .detoxrc."),
|
|
2008
|
+
explainOnFailure: z.boolean().optional().describe("Se true, quando falhar gera automaticamente: O que aconteceu, Por que falhou, O que fazer, Sugest\xE3o de corre\xE7\xE3o. Requer API key."),
|
|
2009
|
+
autoFixSelector: z.boolean().optional().describe("Se true e falhar por seletor, aplica corre\xE7\xE3o automaticamente e tenta novamente. Requer spec e API key. Default: true para mobile.")
|
|
1401
2010
|
}),
|
|
1402
2011
|
outputSchema: z.object({
|
|
1403
2012
|
status: z.enum(["passed", "failed", "not_found"]),
|
|
@@ -1406,7 +2015,7 @@ server.registerTool(
|
|
|
1406
2015
|
runOutput: z.string().optional()
|
|
1407
2016
|
})
|
|
1408
2017
|
},
|
|
1409
|
-
async ({ framework, spec, suite, explainOnFailure }) => {
|
|
2018
|
+
async ({ framework, spec, suite, explainOnFailure, device, autoFixSelector }) => {
|
|
1410
2019
|
const structure = detectProjectStructure();
|
|
1411
2020
|
if (!structure.hasTests) {
|
|
1412
2021
|
return {
|
|
@@ -1422,15 +2031,28 @@ server.registerTool(
|
|
|
1422
2031
|
if (!selectedFramework && structure.testFrameworks.length > 0) {
|
|
1423
2032
|
selectedFramework = structure.testFrameworks[0];
|
|
1424
2033
|
}
|
|
2034
|
+
const deviceConfig = structure.hasMobile ? detectDeviceConfig(structure) : {};
|
|
2035
|
+
const useDevice = device || deviceConfig.configuration || deviceConfig.device;
|
|
2036
|
+
const doAutoFixSelector = autoFixSelector ?? (structure.hasMobile && !!spec);
|
|
2037
|
+
let runEnv = { ...process.env };
|
|
2038
|
+
if (useDevice && Object.keys(deviceConfig.envOverrides || {}).length) {
|
|
2039
|
+
runEnv = { ...runEnv, ...deviceConfig.envOverrides };
|
|
2040
|
+
}
|
|
2041
|
+
if (device) {
|
|
2042
|
+
if (selectedFramework === "detox") runEnv.DETOX_CONFIGURATION = device;
|
|
2043
|
+
else if (selectedFramework === "appium") runEnv.APPIUM_DEVICE_NAME = device;
|
|
2044
|
+
} else if (deviceConfig.configuration && selectedFramework === "detox") {
|
|
2045
|
+
runEnv.DETOX_CONFIGURATION = deviceConfig.configuration;
|
|
2046
|
+
}
|
|
1425
2047
|
let cmd, args, cwd;
|
|
1426
2048
|
if (selectedFramework === "cypress") {
|
|
1427
2049
|
cmd = "npx";
|
|
1428
2050
|
args = spec ? ["cypress", "run", "--spec", spec] : ["cypress", "run"];
|
|
1429
|
-
cwd = structure.testDirs.includes("cypress") ?
|
|
2051
|
+
cwd = structure.testDirs.includes("cypress") ? path6.join(PROJECT_ROOT6, "cypress") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
|
|
1430
2052
|
} else if (selectedFramework === "playwright") {
|
|
1431
2053
|
cmd = "npx";
|
|
1432
2054
|
args = spec ? ["playwright", "test", spec] : ["playwright", "test"];
|
|
1433
|
-
cwd = structure.testDirs.includes("playwright") ?
|
|
2055
|
+
cwd = structure.testDirs.includes("playwright") ? path6.join(PROJECT_ROOT6, "playwright") : structure.testDirs[0] ? path6.join(PROJECT_ROOT6, structure.testDirs[0]) : PROJECT_ROOT6;
|
|
1434
2056
|
} else if (selectedFramework === "webdriverio") {
|
|
1435
2057
|
cmd = "npx";
|
|
1436
2058
|
args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
|
|
@@ -1455,108 +2077,138 @@ server.registerTool(
|
|
|
1455
2077
|
cmd = "npx";
|
|
1456
2078
|
args = ["jest"];
|
|
1457
2079
|
if (spec) args.push(spec);
|
|
1458
|
-
cwd =
|
|
2080
|
+
cwd = PROJECT_ROOT6;
|
|
1459
2081
|
} else if (selectedFramework === "vitest") {
|
|
1460
2082
|
cmd = "npx";
|
|
1461
2083
|
args = ["vitest", "run"];
|
|
1462
2084
|
if (spec) args.push(spec);
|
|
1463
|
-
cwd =
|
|
2085
|
+
cwd = PROJECT_ROOT6;
|
|
1464
2086
|
} else if (selectedFramework === "mocha") {
|
|
1465
2087
|
cmd = "npx";
|
|
1466
2088
|
args = spec ? ["mocha", spec] : ["mocha"];
|
|
1467
|
-
cwd =
|
|
2089
|
+
cwd = PROJECT_ROOT6;
|
|
1468
2090
|
} else if (selectedFramework === "appium") {
|
|
1469
2091
|
cmd = "npx";
|
|
1470
2092
|
args = spec ? ["wdio", "run", spec] : ["wdio", "run"];
|
|
1471
|
-
cwd =
|
|
2093
|
+
cwd = PROJECT_ROOT6;
|
|
1472
2094
|
} else if (selectedFramework === "detox") {
|
|
1473
2095
|
cmd = "npx";
|
|
1474
2096
|
args = ["detox", "test"];
|
|
2097
|
+
if (useDevice) args.push("--configuration", useDevice);
|
|
1475
2098
|
if (spec) args.push(spec);
|
|
1476
|
-
cwd =
|
|
2099
|
+
cwd = PROJECT_ROOT6;
|
|
1477
2100
|
} else if (selectedFramework === "robot") {
|
|
1478
2101
|
cmd = "robot";
|
|
1479
2102
|
args = spec ? [spec] : [structure.testDirs[0] || "tests"];
|
|
1480
|
-
cwd =
|
|
2103
|
+
cwd = PROJECT_ROOT6;
|
|
1481
2104
|
} else if (selectedFramework === "pytest") {
|
|
1482
2105
|
cmd = "pytest";
|
|
1483
2106
|
args = spec ? [spec] : [];
|
|
1484
|
-
cwd =
|
|
2107
|
+
cwd = PROJECT_ROOT6;
|
|
1485
2108
|
} else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
|
|
1486
2109
|
cmd = "npm";
|
|
1487
2110
|
args = ["test"];
|
|
1488
|
-
cwd =
|
|
2111
|
+
cwd = PROJECT_ROOT6;
|
|
1489
2112
|
} else {
|
|
1490
2113
|
cmd = "npm";
|
|
1491
2114
|
args = ["test"];
|
|
1492
|
-
cwd =
|
|
2115
|
+
cwd = PROJECT_ROOT6;
|
|
1493
2116
|
}
|
|
1494
|
-
const
|
|
1495
|
-
|
|
2117
|
+
const runTestsOnce2 = () => new Promise((resolve) => {
|
|
2118
|
+
const startTime = Date.now();
|
|
1496
2119
|
const child = spawn2(cmd, args, {
|
|
1497
2120
|
cwd,
|
|
1498
2121
|
stdio: ["inherit", "pipe", "pipe"],
|
|
1499
2122
|
shell: process.platform === "win32",
|
|
1500
|
-
env:
|
|
2123
|
+
env: runEnv
|
|
1501
2124
|
});
|
|
1502
2125
|
let stdout = "";
|
|
1503
2126
|
let stderr = "";
|
|
1504
|
-
if (child.stdout) {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
child.stderr.on("data", (d) => {
|
|
1513
|
-
const s = d.toString();
|
|
1514
|
-
stderr += s;
|
|
1515
|
-
process.stderr.write(s);
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
2127
|
+
if (child.stdout) child.stdout.on("data", (d) => {
|
|
2128
|
+
stdout += d.toString();
|
|
2129
|
+
process.stdout.write(d);
|
|
2130
|
+
});
|
|
2131
|
+
if (child.stderr) child.stderr.on("data", (d) => {
|
|
2132
|
+
stderr += d.toString();
|
|
2133
|
+
process.stderr.write(d);
|
|
2134
|
+
});
|
|
1518
2135
|
child.on("close", (code) => {
|
|
1519
2136
|
const runOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
1520
|
-
const passed = code === 0;
|
|
1521
|
-
const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
|
|
1522
|
-
if (!passed && runOutput) {
|
|
1523
|
-
try {
|
|
1524
|
-
fs5.writeFileSync(path5.join(PROJECT_ROOT5, ".qa-lab-last-failure.log"), runOutput, "utf8");
|
|
1525
|
-
} catch {
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
const { passed: p, failed: f } = parseTestRunResult(runOutput, code);
|
|
1529
|
-
appendMetricsEvent({
|
|
1530
|
-
type: "test_run",
|
|
1531
|
-
framework: selectedFramework,
|
|
1532
|
-
spec: spec || void 0,
|
|
1533
|
-
passed: p,
|
|
1534
|
-
failed: f,
|
|
1535
|
-
durationSeconds,
|
|
1536
|
-
exitCode: code ?? 1,
|
|
1537
|
-
failures: !passed ? extractFailuresFromOutput(runOutput) : void 0
|
|
1538
|
-
});
|
|
1539
|
-
if (passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
|
|
1540
|
-
saveProjectMemory({
|
|
1541
|
-
execution: {
|
|
1542
|
-
testFile: spec || "all",
|
|
1543
|
-
passed,
|
|
1544
|
-
duration: durationSeconds,
|
|
1545
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1546
|
-
framework: selectedFramework
|
|
1547
|
-
}
|
|
1548
|
-
});
|
|
1549
2137
|
resolve({
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
exitCode: code ?? 1,
|
|
1555
|
-
runOutput: !passed ? runOutput : void 0
|
|
1556
|
-
}
|
|
2138
|
+
passed: code === 0,
|
|
2139
|
+
exitCode: code ?? 1,
|
|
2140
|
+
runOutput,
|
|
2141
|
+
durationSeconds: Math.round((Date.now() - startTime) / 1e3)
|
|
1557
2142
|
});
|
|
1558
2143
|
});
|
|
1559
2144
|
});
|
|
2145
|
+
const isSelectorFailure = (out) => /element not found|selector|timeout|locator|cy\.get|page\.locator|Unable to find/i.test(out || "");
|
|
2146
|
+
let result = await runTestsOnce2();
|
|
2147
|
+
let autoFixed = false;
|
|
2148
|
+
if (!result.passed && doAutoFixSelector && spec && isSelectorFailure(result.runOutput) && resolveLLMProvider("complex").apiKey) {
|
|
2149
|
+
const fixResult = await applySelectorFixAndRetry(spec, result.runOutput, selectedFramework);
|
|
2150
|
+
if (fixResult.applied) {
|
|
2151
|
+
autoFixed = true;
|
|
2152
|
+
result = await runTestsOnce2();
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
if (!result.passed && result.runOutput) {
|
|
2156
|
+
try {
|
|
2157
|
+
fs6.writeFileSync(path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log"), result.runOutput, "utf8");
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
const { passed: p, failed: f } = parseTestRunResult(result.runOutput, result.exitCode);
|
|
2162
|
+
appendMetricsEvent({
|
|
2163
|
+
type: "test_run",
|
|
2164
|
+
framework: selectedFramework,
|
|
2165
|
+
spec: spec || void 0,
|
|
2166
|
+
passed: p,
|
|
2167
|
+
failed: f,
|
|
2168
|
+
durationSeconds: result.durationSeconds,
|
|
2169
|
+
exitCode: result.exitCode,
|
|
2170
|
+
failures: !result.passed ? extractFailuresFromOutput(result.runOutput) : void 0
|
|
2171
|
+
});
|
|
2172
|
+
if (result.passed) saveProjectMemory({ lastRun: { spec: spec || null, framework: selectedFramework, passed: p } });
|
|
2173
|
+
saveProjectMemory({
|
|
2174
|
+
execution: {
|
|
2175
|
+
testFile: spec || "all",
|
|
2176
|
+
passed: result.passed,
|
|
2177
|
+
duration: result.durationSeconds,
|
|
2178
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2179
|
+
framework: selectedFramework
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
const baseMsg = result.passed ? autoFixed ? "Testes executados com sucesso (ap\xF3s corre\xE7\xE3o autom\xE1tica de seletor)." : "Testes executados com sucesso." : "Falha na execu\xE7\xE3o dos testes.";
|
|
2183
|
+
const structured = {
|
|
2184
|
+
status: result.passed ? "passed" : "failed",
|
|
2185
|
+
message: result.passed ? "Tests passed" : "Tests failed",
|
|
2186
|
+
exitCode: result.exitCode,
|
|
2187
|
+
runOutput: !result.passed ? result.runOutput : void 0,
|
|
2188
|
+
autoFixed: autoFixed || void 0
|
|
2189
|
+
};
|
|
2190
|
+
if (!result.passed && explainOnFailure && result.runOutput) {
|
|
2191
|
+
const explainResult = await generateFailureExplanation(result.runOutput, spec || void 0);
|
|
2192
|
+
if (explainResult.ok && explainResult.structuredContent) {
|
|
2193
|
+
const oneLine = explainResult.structuredContent.resumoEmUmaFrase || oneLineFailureSummary(result.runOutput, selectedFramework, explainResult.structuredContent.oQueAconteceu, explainResult.structuredContent.sugestaoCorrecao);
|
|
2194
|
+
structured.explanation = explainResult.structuredContent.formattedText;
|
|
2195
|
+
structured.resumoEmUmaFrase = oneLine;
|
|
2196
|
+
return {
|
|
2197
|
+
content: [{ type: "text", text: `${baseMsg}
|
|
2198
|
+
|
|
2199
|
+
**${oneLine}**
|
|
2200
|
+
|
|
2201
|
+
---
|
|
2202
|
+
|
|
2203
|
+
${explainResult.structuredContent.formattedText}` }],
|
|
2204
|
+
structuredContent: structured
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return {
|
|
2209
|
+
content: [{ type: "text", text: baseMsg }],
|
|
2210
|
+
structuredContent: structured
|
|
2211
|
+
};
|
|
1560
2212
|
}
|
|
1561
2213
|
);
|
|
1562
2214
|
server.registerTool(
|
|
@@ -1645,10 +2297,10 @@ server.registerTool(
|
|
|
1645
2297
|
${referenceCode.slice(0, 8e3)}`;
|
|
1646
2298
|
if (referencePaths?.length) {
|
|
1647
2299
|
for (const p of referencePaths.slice(0, 5)) {
|
|
1648
|
-
const full =
|
|
1649
|
-
if (
|
|
2300
|
+
const full = path6.join(PROJECT_ROOT6, p.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2301
|
+
if (fs6.existsSync(full)) {
|
|
1650
2302
|
try {
|
|
1651
|
-
const content =
|
|
2303
|
+
const content = fs6.readFileSync(full, "utf8");
|
|
1652
2304
|
referenceBlock += `
|
|
1653
2305
|
|
|
1654
2306
|
--- Arquivo: ${p} ---
|
|
@@ -1681,7 +2333,9 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
|
|
|
1681
2333
|
|
|
1682
2334
|
${UNIVERSAL_TEST_PRACTICES}
|
|
1683
2335
|
${fw === "appium" || fw === "detox" ? `
|
|
1684
|
-
IMPORTANTE: ${MOBILE_MAPPING_LESSON}
|
|
2336
|
+
IMPORTANTE: ${MOBILE_MAPPING_LESSON}
|
|
2337
|
+
|
|
2338
|
+
HIERARQUIA DE SELETORES: ${MOBILE_SELECTOR_HIERARCHY}` : ""}` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
|
|
1685
2339
|
Framework: ${fw}
|
|
1686
2340
|
|
|
1687
2341
|
${UNIVERSAL_TEST_PRACTICES}
|
|
@@ -1695,7 +2349,9 @@ Regras:
|
|
|
1695
2349
|
- pytest: def test_*, assert, fixtures
|
|
1696
2350
|
- C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown${fw === "appium" || fw === "detox" ? `
|
|
1697
2351
|
|
|
1698
|
-
IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}
|
|
2352
|
+
IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}
|
|
2353
|
+
|
|
2354
|
+
HIERARQUIA: ${MOBILE_SELECTOR_HIERARCHY}` : ""}`;
|
|
1699
2355
|
const userPrompt = `Contexto do projeto:
|
|
1700
2356
|
${contextWithMemory.slice(0, 5e3)}
|
|
1701
2357
|
|
|
@@ -1779,7 +2435,7 @@ function getExtensionAndBaseDir2(fw, structure) {
|
|
|
1779
2435
|
robot: structure.testDirs.includes("robot") ? "robot" : structure.testDirs[0] || "tests",
|
|
1780
2436
|
behave: structure.testDirs.includes("features") ? "features" : structure.testDirs[0] || "tests"
|
|
1781
2437
|
};
|
|
1782
|
-
const baseDir =
|
|
2438
|
+
const baseDir = path6.join(PROJECT_ROOT6, baseMap[fw] || structure.testDirs[0] || "tests");
|
|
1783
2439
|
return { ext, baseDir };
|
|
1784
2440
|
}
|
|
1785
2441
|
server.registerTool(
|
|
@@ -1824,13 +2480,13 @@ server.registerTool(
|
|
|
1824
2480
|
const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
|
|
1825
2481
|
const safeName = name.replace(/[^a-z0-9-_]/gi, "-").replace(/-+/g, "-").replace(/_+/g, "_").replace(/\.(cy|spec|test|robot|feature|py)\.?(js|ts|py)?$/i, "").replace(/^[-_]+|[-_]+$/g, "");
|
|
1826
2482
|
const fileName = ext.startsWith("_") ? `${safeName}${ext}` : `${safeName}${ext}`;
|
|
1827
|
-
const targetDir = subdir ?
|
|
1828
|
-
const filePath =
|
|
2483
|
+
const targetDir = subdir ? path6.join(baseDir, subdir) : baseDir;
|
|
2484
|
+
const filePath = path6.join(targetDir, fileName);
|
|
1829
2485
|
try {
|
|
1830
|
-
if (!
|
|
1831
|
-
|
|
2486
|
+
if (!fs6.existsSync(targetDir)) {
|
|
2487
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
1832
2488
|
}
|
|
1833
|
-
|
|
2489
|
+
fs6.writeFileSync(filePath, content, "utf8");
|
|
1834
2490
|
return {
|
|
1835
2491
|
content: [{ type: "text", text: `Arquivo gravado: ${filePath}` }],
|
|
1836
2492
|
structuredContent: { ok: true, path: filePath }
|
|
@@ -1900,8 +2556,10 @@ server.registerTool(
|
|
|
1900
2556
|
};
|
|
1901
2557
|
}
|
|
1902
2558
|
);
|
|
1903
|
-
function formatFailureExplanation(data) {
|
|
1904
|
-
const
|
|
2559
|
+
function formatFailureExplanation(data, oneLine = null) {
|
|
2560
|
+
const summary = oneLine || data.resumoEmUmaFrase || "";
|
|
2561
|
+
const lines = summary ? [`**${summary}**`, "", "---", ""] : [];
|
|
2562
|
+
lines.push(
|
|
1905
2563
|
"## O que aconteceu",
|
|
1906
2564
|
"",
|
|
1907
2565
|
data.oQueAconteceu || "",
|
|
@@ -1913,7 +2571,7 @@ function formatFailureExplanation(data) {
|
|
|
1913
2571
|
"## O que fazer agora",
|
|
1914
2572
|
"",
|
|
1915
2573
|
...Array.isArray(data.oQueFazerAgora) ? data.oQueFazerAgora.map((s, i) => `${i + 1}. ${s}`) : [data.oQueFazerAgora || ""]
|
|
1916
|
-
|
|
2574
|
+
);
|
|
1917
2575
|
if (data.sugestaoCorrecao) {
|
|
1918
2576
|
lines.push("", "## Sugest\xE3o de corre\xE7\xE3o", "", "```" + (data.framework || "js"), data.sugestaoCorrecao, "```");
|
|
1919
2577
|
}
|
|
@@ -1955,6 +2613,70 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
|
|
|
1955
2613
|
const data = await res.json();
|
|
1956
2614
|
return data.choices?.[0]?.message?.content || "";
|
|
1957
2615
|
}
|
|
2616
|
+
async function generateFailureExplanation(resolvedOutput, testFilePath = null) {
|
|
2617
|
+
const structure = detectProjectStructure();
|
|
2618
|
+
const fw = structure.testFrameworks[0] || "unknown";
|
|
2619
|
+
let testCode = "";
|
|
2620
|
+
if (testFilePath) {
|
|
2621
|
+
const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
|
|
2622
|
+
const fullPath = path6.join(PROJECT_ROOT6, normalized);
|
|
2623
|
+
if (fs6.existsSync(fullPath) && !fs6.statSync(fullPath).isDirectory()) {
|
|
2624
|
+
try {
|
|
2625
|
+
testCode = fs6.readFileSync(fullPath, "utf8");
|
|
2626
|
+
} catch {
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
const llm = resolveLLMProvider("complex");
|
|
2631
|
+
if (!llm.apiKey) return { ok: false, structuredContent: null };
|
|
2632
|
+
const { provider, apiKey, baseUrl, model } = llm;
|
|
2633
|
+
const fwHints = {
|
|
2634
|
+
webdriverio: "WebdriverIO (describe/it, $, browser.$)",
|
|
2635
|
+
appium: "Appium/WebdriverIO (mobile, $, browser.$)",
|
|
2636
|
+
playwright: "Playwright (test, page, locator)",
|
|
2637
|
+
cypress: "Cypress (cy.get, cy.click)",
|
|
2638
|
+
jest: "Jest (describe, test, expect)",
|
|
2639
|
+
vitest: "Vitest (describe, test, expect)",
|
|
2640
|
+
robot: "Robot Framework",
|
|
2641
|
+
pytest: "pytest"
|
|
2642
|
+
};
|
|
2643
|
+
const systemPrompt = `Voc\xEA \xE9 um mentor de QA. Analise o output de falha e responda em JSON (apenas o JSON, sem markdown) com as chaves:
|
|
2644
|
+
- resumoEmUmaFrase: string (OBRIGAT\xD3RIO - uma frase: "Falhou porque X. Solu\xE7\xE3o: Y.")
|
|
2645
|
+
- oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
|
|
2646
|
+
- porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas)
|
|
2647
|
+
- oQueFazerAgora: array de strings (passos numerados do que fazer)
|
|
2648
|
+
- sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o no formato do framework)
|
|
2649
|
+
- conceito: string ou null
|
|
2650
|
+
- framework: string (framework do projeto)
|
|
2651
|
+
|
|
2652
|
+
Framework: ${fw}. ${fwHints[fw] || ""}
|
|
2653
|
+
Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
|
|
2654
|
+
const userPrompt = `Output do terminal/log (teste falhou):
|
|
2655
|
+
---
|
|
2656
|
+
${resolvedOutput.slice(0, 12e3)}
|
|
2657
|
+
---
|
|
2658
|
+
${testCode ? `
|
|
2659
|
+
C\xF3digo do teste:
|
|
2660
|
+
---
|
|
2661
|
+
${testCode.slice(0, 6e3)}
|
|
2662
|
+
---` : ""}`;
|
|
2663
|
+
try {
|
|
2664
|
+
let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
|
|
2665
|
+
raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
|
|
2666
|
+
let data = {};
|
|
2667
|
+
try {
|
|
2668
|
+
data = JSON.parse(raw);
|
|
2669
|
+
} catch {
|
|
2670
|
+
data = { oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear.", porQueProvavelmenteFalhou: [], oQueFazerAgora: [], sugestaoCorrecao: null, conceito: null, framework: fw };
|
|
2671
|
+
}
|
|
2672
|
+
data.framework = data.framework || fw;
|
|
2673
|
+
const oneLine = oneLineFailureSummary(resolvedOutput, fw, data.oQueAconteceu, data.sugestaoCorrecao);
|
|
2674
|
+
const formattedText = formatFailureExplanation(data, data.resumoEmUmaFrase || oneLine);
|
|
2675
|
+
return { ok: true, formattedText, structuredContent: { ...data, formattedText } };
|
|
2676
|
+
} catch (err) {
|
|
2677
|
+
return { ok: false, error: err.message, structuredContent: null };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
1958
2680
|
server.registerTool(
|
|
1959
2681
|
"por_que_falhou",
|
|
1960
2682
|
{
|
|
@@ -1977,14 +2699,12 @@ server.registerTool(
|
|
|
1977
2699
|
})
|
|
1978
2700
|
},
|
|
1979
2701
|
async ({ errorOutput, testFilePath }) => {
|
|
1980
|
-
const structure = detectProjectStructure();
|
|
1981
|
-
const fw = structure.testFrameworks[0] || "unknown";
|
|
1982
2702
|
let resolvedOutput = errorOutput?.trim() || "";
|
|
1983
2703
|
if (!resolvedOutput) {
|
|
1984
|
-
const lastFailurePath =
|
|
1985
|
-
if (
|
|
2704
|
+
const lastFailurePath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
|
|
2705
|
+
if (fs6.existsSync(lastFailurePath)) {
|
|
1986
2706
|
try {
|
|
1987
|
-
resolvedOutput =
|
|
2707
|
+
resolvedOutput = fs6.readFileSync(lastFailurePath, "utf8");
|
|
1988
2708
|
} catch {
|
|
1989
2709
|
}
|
|
1990
2710
|
}
|
|
@@ -1998,94 +2718,36 @@ server.registerTool(
|
|
|
1998
2718
|
structuredContent: { ok: false, error: "No error output" }
|
|
1999
2719
|
};
|
|
2000
2720
|
}
|
|
2001
|
-
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
const llm = resolveLLMProvider("complex");
|
|
2013
|
-
if (!llm.apiKey) {
|
|
2014
|
-
return {
|
|
2015
|
-
content: [{
|
|
2016
|
-
type: "text",
|
|
2017
|
-
text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
|
|
2018
|
-
}],
|
|
2019
|
-
structuredContent: { ok: false, error: "No API key configured" }
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
|
-
const { provider, apiKey, baseUrl, model } = llm;
|
|
2023
|
-
const fwHints = {
|
|
2024
|
-
webdriverio: "WebdriverIO (describe/it, $, browser.$)",
|
|
2025
|
-
appium: "Appium/WebdriverIO (mobile, $, browser.$)",
|
|
2026
|
-
playwright: "Playwright (test, page, locator)",
|
|
2027
|
-
cypress: "Cypress (cy.get, cy.click)",
|
|
2028
|
-
jest: "Jest (describe, test, expect)",
|
|
2029
|
-
vitest: "Vitest (describe, test, expect)",
|
|
2030
|
-
robot: "Robot Framework",
|
|
2031
|
-
pytest: "pytest"
|
|
2032
|
-
};
|
|
2033
|
-
const systemPrompt = `Voc\xEA \xE9 um mentor de QA. Analise o output de falha e responda em JSON (apenas o JSON, sem markdown) com as chaves:
|
|
2034
|
-
- oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
|
|
2035
|
-
- porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas, uma por item)
|
|
2036
|
-
- oQueFazerAgora: array de strings (passos numerados do que fazer)
|
|
2037
|
-
- sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o se aplic\xE1vel, no formato do framework)
|
|
2038
|
-
- conceito: string ou null (ex: "Flaky test = teste intermitente. Geralmente por timing ou seletores fr\xE1geis.")
|
|
2039
|
-
- framework: string (framework do projeto)
|
|
2040
|
-
|
|
2041
|
-
Framework do projeto: ${fw}. ${fwHints[fw] || ""}
|
|
2042
|
-
Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
|
|
2043
|
-
const userPrompt = `Output do terminal/log (teste falhou):
|
|
2044
|
-
---
|
|
2045
|
-
${resolvedOutput.slice(0, 12e3)}
|
|
2046
|
-
---
|
|
2047
|
-
${testCode ? `
|
|
2048
|
-
C\xF3digo do teste que falhou:
|
|
2049
|
-
---
|
|
2050
|
-
${testCode.slice(0, 6e3)}
|
|
2051
|
-
---` : ""}`;
|
|
2052
|
-
try {
|
|
2053
|
-
let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
|
|
2054
|
-
raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
|
|
2055
|
-
let data = {};
|
|
2056
|
-
try {
|
|
2057
|
-
data = JSON.parse(raw);
|
|
2058
|
-
} catch {
|
|
2059
|
-
data = {
|
|
2060
|
-
oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear a resposta.",
|
|
2061
|
-
porQueProvavelmenteFalhou: [],
|
|
2062
|
-
oQueFazerAgora: [],
|
|
2063
|
-
sugestaoCorrecao: null,
|
|
2064
|
-
conceito: null,
|
|
2065
|
-
framework: fw
|
|
2721
|
+
const explainResult = await generateFailureExplanation(resolvedOutput, testFilePath);
|
|
2722
|
+
if (!explainResult.ok) {
|
|
2723
|
+
if (!resolveLLMProvider("complex").apiKey) {
|
|
2724
|
+
return {
|
|
2725
|
+
content: [{
|
|
2726
|
+
type: "text",
|
|
2727
|
+
text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env do projeto para usar a explica\xE7\xE3o com LLM."
|
|
2728
|
+
}],
|
|
2729
|
+
structuredContent: { ok: false, error: "No API key configured" }
|
|
2066
2730
|
};
|
|
2067
2731
|
}
|
|
2068
|
-
data.framework = data.framework || fw;
|
|
2069
|
-
const formattedText = formatFailureExplanation(data);
|
|
2070
2732
|
return {
|
|
2071
|
-
content: [{ type: "text", text:
|
|
2072
|
-
structuredContent: {
|
|
2073
|
-
ok: true,
|
|
2074
|
-
oQueAconteceu: data.oQueAconteceu,
|
|
2075
|
-
porQueProvavelmenteFalhou: data.porQueProvavelmenteFalhou,
|
|
2076
|
-
oQueFazerAgora: data.oQueFazerAgora,
|
|
2077
|
-
sugestaoCorrecao: data.sugestaoCorrecao ?? null,
|
|
2078
|
-
conceito: data.conceito ?? null,
|
|
2079
|
-
framework: data.framework,
|
|
2080
|
-
formattedText
|
|
2081
|
-
}
|
|
2082
|
-
};
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
return {
|
|
2085
|
-
content: [{ type: "text", text: `Erro ao analisar: ${err.message}` }],
|
|
2086
|
-
structuredContent: { ok: false, error: err.message }
|
|
2733
|
+
content: [{ type: "text", text: `Erro ao analisar: ${explainResult.error || "erro desconhecido"}` }],
|
|
2734
|
+
structuredContent: { ok: false, error: explainResult.error }
|
|
2087
2735
|
};
|
|
2088
2736
|
}
|
|
2737
|
+
const sc = explainResult.structuredContent;
|
|
2738
|
+
return {
|
|
2739
|
+
content: [{ type: "text", text: sc.formattedText }],
|
|
2740
|
+
structuredContent: {
|
|
2741
|
+
ok: true,
|
|
2742
|
+
oQueAconteceu: sc.oQueAconteceu,
|
|
2743
|
+
porQueProvavelmenteFalhou: sc.porQueProvavelmenteFalhou,
|
|
2744
|
+
oQueFazerAgora: sc.oQueFazerAgora,
|
|
2745
|
+
sugestaoCorrecao: sc.sugestaoCorrecao ?? null,
|
|
2746
|
+
conceito: sc.conceito ?? null,
|
|
2747
|
+
framework: sc.framework,
|
|
2748
|
+
formattedText: sc.formattedText
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2089
2751
|
}
|
|
2090
2752
|
);
|
|
2091
2753
|
server.registerTool(
|
|
@@ -2153,7 +2815,7 @@ server.registerTool(
|
|
|
2153
2815
|
inputSchema: z.object({
|
|
2154
2816
|
testFilePath: z.string().describe("Caminho do arquivo de teste que falhou (ex: specs/login.spec.js)."),
|
|
2155
2817
|
errorOutput: z.string().optional().describe("Output do terminal da falha. Se vazio, l\xEA de .qa-lab-last-failure.log."),
|
|
2156
|
-
framework: z.enum(["cypress", "playwright", "webdriverio", "appium"]).optional().describe("Framework do teste. Detectado automaticamente se omitido.")
|
|
2818
|
+
framework: z.enum(["cypress", "playwright", "webdriverio", "appium", "detox"]).optional().describe("Framework do teste. Detectado automaticamente se omitido.")
|
|
2157
2819
|
}),
|
|
2158
2820
|
outputSchema: z.object({
|
|
2159
2821
|
ok: z.boolean(),
|
|
@@ -2168,9 +2830,9 @@ server.registerTool(
|
|
|
2168
2830
|
const fw = framework || inferFrameworkFromFile(testFilePath.split("/").pop(), structure);
|
|
2169
2831
|
let resolvedOutput = errorOutput;
|
|
2170
2832
|
if (!resolvedOutput) {
|
|
2171
|
-
const logPath =
|
|
2172
|
-
if (
|
|
2173
|
-
resolvedOutput =
|
|
2833
|
+
const logPath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
|
|
2834
|
+
if (fs6.existsSync(logPath)) {
|
|
2835
|
+
resolvedOutput = fs6.readFileSync(logPath, "utf8");
|
|
2174
2836
|
}
|
|
2175
2837
|
}
|
|
2176
2838
|
if (!resolvedOutput) {
|
|
@@ -2186,10 +2848,10 @@ server.registerTool(
|
|
|
2186
2848
|
};
|
|
2187
2849
|
}
|
|
2188
2850
|
let testCode = "";
|
|
2189
|
-
const fullPath =
|
|
2190
|
-
if (
|
|
2851
|
+
const fullPath = path6.join(PROJECT_ROOT6, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2852
|
+
if (fs6.existsSync(fullPath)) {
|
|
2191
2853
|
try {
|
|
2192
|
-
testCode =
|
|
2854
|
+
testCode = fs6.readFileSync(fullPath, "utf8");
|
|
2193
2855
|
} catch {
|
|
2194
2856
|
}
|
|
2195
2857
|
}
|
|
@@ -2205,15 +2867,18 @@ server.registerTool(
|
|
|
2205
2867
|
cypress: "Cypress: cy.get('[data-testid=...]'), cy.contains(), cy.get('button').filter(':visible')",
|
|
2206
2868
|
playwright: `Playwright: page.getByRole(), page.getByTestId(), page.locator('button:has-text("...")')`,
|
|
2207
2869
|
webdriverio: "WebdriverIO: $('[data-testid=...]'), $('button=Texto')",
|
|
2208
|
-
appium:
|
|
2870
|
+
appium: `Appium (HIERARQUIA \xDANICA): 1) id: $('~accessibility-id') ou $('~content-desc'). 2) XPath relacional: \xE2ncora est\xE1vel + eixos + TIPO ESPEC\xCDFICO (android.widget.Button, XCUIElementTypeButton). NUNCA use * em XPath \u2014 quebra por timing e m\xFAltiplos matches. Ex: //android.widget.LinearLayout[@resource-id='login_form']/descendant::android.widget.Button[@text='Entrar']. 3) resource-id. Explique a hierarquia.`,
|
|
2871
|
+
detox: `Detox: testID > accessibilityLabel > text. Explique por que \xE9 mais est\xE1vel.`
|
|
2209
2872
|
};
|
|
2873
|
+
const mobileRules = fw === "appium" || fw === "detox" ? "\n\nMOBILE: 1) id. 2) XPath relacional: \xE2ncora + eixos + TIPO ESPEC\xCDFICO (android.widget.Button, XCUIElementTypeButton). NUNCA use * \u2014 quebra por timing. Ex: //android.widget.LinearLayout[@resource-id='login_form']/descendant::android.widget.Button[@text='Entrar']. 3) resource-id. Explique por que o seletor \xE9 forte." : "";
|
|
2210
2874
|
const systemPrompt = `Voc\xEA \xE9 um especialista em testes E2E. O teste falhou porque um seletor n\xE3o encontrou o elemento (UI mudou).
|
|
2211
2875
|
Analise o erro e o c\xF3digo e responda APENAS em JSON (sem markdown) com as chaves:
|
|
2212
2876
|
- selectorSugerido: string (o novo seletor recomendado, mais resiliente)
|
|
2213
2877
|
- codigoCorrigido: string (bloco de c\xF3digo completo corrigido, apenas a parte relevante do teste)
|
|
2214
|
-
- explicacao: string (breve explica\xE7\xE3o em portugu\xEAs: por que o antigo falhou e por que o novo \xE9 melhor)
|
|
2878
|
+
- explicacao: string (breve explica\xE7\xE3o em portugu\xEAs: por que o antigo falhou e por que o novo \xE9 melhor. Em mobile: mencione a hierarquia de estabilidade)
|
|
2215
2879
|
|
|
2216
2880
|
Priorize nesta ordem: data-testid > role + accessible name > texto vis\xEDvel > estrutura. Evite classes CSS e IDs que mudam.
|
|
2881
|
+
${mobileRules}
|
|
2217
2882
|
|
|
2218
2883
|
Framework: ${fw}. ${fwHints[fw] || ""}`;
|
|
2219
2884
|
const userPrompt = `Output do erro:
|
|
@@ -2298,10 +2963,10 @@ server.registerTool(
|
|
|
2298
2963
|
let instructions = "";
|
|
2299
2964
|
let contextForGenerate = "";
|
|
2300
2965
|
if (elementsJsonPath) {
|
|
2301
|
-
const fullPath =
|
|
2302
|
-
if (
|
|
2966
|
+
const fullPath = path6.join(PROJECT_ROOT6, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2967
|
+
if (fs6.existsSync(fullPath)) {
|
|
2303
2968
|
try {
|
|
2304
|
-
const raw =
|
|
2969
|
+
const raw = fs6.readFileSync(fullPath, "utf8");
|
|
2305
2970
|
const parsed = JSON.parse(raw);
|
|
2306
2971
|
const arr = Array.isArray(parsed) ? parsed : parsed.elements || parsed.items || [];
|
|
2307
2972
|
arr.forEach((el) => {
|
|
@@ -2402,20 +3067,20 @@ server.registerTool(
|
|
|
2402
3067
|
},
|
|
2403
3068
|
async ({ path: filePath }) => {
|
|
2404
3069
|
const normalized = filePath.replace(/^\//, "").replace(/\\/g, "/");
|
|
2405
|
-
const fullPath =
|
|
2406
|
-
if (!fullPath.startsWith(
|
|
3070
|
+
const fullPath = path6.join(PROJECT_ROOT6, normalized);
|
|
3071
|
+
if (!fullPath.startsWith(PROJECT_ROOT6)) {
|
|
2407
3072
|
return {
|
|
2408
3073
|
content: [{ type: "text", text: "Caminho fora do projeto." }],
|
|
2409
3074
|
structuredContent: { ok: false, error: "Path outside project" }
|
|
2410
3075
|
};
|
|
2411
3076
|
}
|
|
2412
|
-
if (!
|
|
3077
|
+
if (!fs6.existsSync(fullPath)) {
|
|
2413
3078
|
return {
|
|
2414
3079
|
content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${normalized}` }],
|
|
2415
3080
|
structuredContent: { ok: false, error: "File not found" }
|
|
2416
3081
|
};
|
|
2417
3082
|
}
|
|
2418
|
-
const stat =
|
|
3083
|
+
const stat = fs6.statSync(fullPath);
|
|
2419
3084
|
if (stat.isDirectory()) {
|
|
2420
3085
|
return {
|
|
2421
3086
|
content: [{ type: "text", text: "\xC9 um diret\xF3rio. Informe um arquivo." }],
|
|
@@ -2424,7 +3089,7 @@ server.registerTool(
|
|
|
2424
3089
|
}
|
|
2425
3090
|
let fileContent = "";
|
|
2426
3091
|
try {
|
|
2427
|
-
fileContent =
|
|
3092
|
+
fileContent = fs6.readFileSync(fullPath, "utf8");
|
|
2428
3093
|
} catch (err) {
|
|
2429
3094
|
return {
|
|
2430
3095
|
content: [{ type: "text", text: `Erro ao ler: ${err.message}` }],
|
|
@@ -2442,7 +3107,7 @@ server.registerTool(
|
|
|
2442
3107
|
};
|
|
2443
3108
|
}
|
|
2444
3109
|
const { provider, apiKey, baseUrl, model } = llm;
|
|
2445
|
-
const ext =
|
|
3110
|
+
const ext = path6.extname(fullPath).toLowerCase();
|
|
2446
3111
|
const lang = [".ts", ".tsx"].includes(ext) ? "TypeScript" : [".js", ".jsx"].includes(ext) ? "JavaScript" : [".py"].includes(ext) ? "Python" : "c\xF3digo";
|
|
2447
3112
|
const systemPrompt = `Voc\xEA \xE9 um revisor de c\xF3digo experiente em QA e testes. Analise o arquivo e cada m\xE9todo/fun\xE7\xE3o, respondendo em JSON v\xE1lido (sem markdown) com a estrutura:
|
|
2448
3113
|
|
|
@@ -2634,9 +3299,9 @@ server.registerTool(
|
|
|
2634
3299
|
const msByPeriod = { "7d": 7 * 24 * 60 * 60 * 1e3, "30d": 30 * 24 * 60 * 60 * 1e3, all: Infinity };
|
|
2635
3300
|
const cutoff = now - msByPeriod[period];
|
|
2636
3301
|
let data = { events: [] };
|
|
2637
|
-
if (
|
|
3302
|
+
if (fs6.existsSync(METRICS_FILE2)) {
|
|
2638
3303
|
try {
|
|
2639
|
-
data = JSON.parse(
|
|
3304
|
+
data = JSON.parse(fs6.readFileSync(METRICS_FILE2, "utf8"));
|
|
2640
3305
|
} catch {
|
|
2641
3306
|
}
|
|
2642
3307
|
}
|
|
@@ -2673,9 +3338,9 @@ server.registerTool(
|
|
|
2673
3338
|
};
|
|
2674
3339
|
}
|
|
2675
3340
|
let flowCoverage = null;
|
|
2676
|
-
if (
|
|
3341
|
+
if (fs6.existsSync(FLOWS_CONFIG_FILE)) {
|
|
2677
3342
|
try {
|
|
2678
|
-
const flowsConfig = JSON.parse(
|
|
3343
|
+
const flowsConfig = JSON.parse(fs6.readFileSync(FLOWS_CONFIG_FILE, "utf8"));
|
|
2679
3344
|
const flows = flowsConfig.flows || [];
|
|
2680
3345
|
const structure = detectProjectStructure();
|
|
2681
3346
|
const allTestFiles = new Set(collectTestFiles(structure).map((e) => e.path));
|
|
@@ -2814,7 +3479,7 @@ server.registerTool(
|
|
|
2814
3479
|
}
|
|
2815
3480
|
return new Promise((resolve) => {
|
|
2816
3481
|
const child = spawn2(cmd, args, {
|
|
2817
|
-
cwd:
|
|
3482
|
+
cwd: PROJECT_ROOT6,
|
|
2818
3483
|
stdio: ["inherit", "pipe", "pipe"],
|
|
2819
3484
|
shell: process.platform === "win32",
|
|
2820
3485
|
env: { ...process.env }
|
|
@@ -2860,13 +3525,13 @@ server.registerTool(
|
|
|
2860
3525
|
async ({ packageManager = "auto" }) => {
|
|
2861
3526
|
let pm = packageManager;
|
|
2862
3527
|
if (pm === "auto") {
|
|
2863
|
-
if (
|
|
2864
|
-
else if (
|
|
3528
|
+
if (fs6.existsSync(path6.join(PROJECT_ROOT6, "yarn.lock"))) pm = "yarn";
|
|
3529
|
+
else if (fs6.existsSync(path6.join(PROJECT_ROOT6, "pnpm-lock.yaml"))) pm = "pnpm";
|
|
2865
3530
|
else pm = "npm";
|
|
2866
3531
|
}
|
|
2867
3532
|
return new Promise((resolve) => {
|
|
2868
3533
|
const child = spawn2(pm, ["install"], {
|
|
2869
|
-
cwd:
|
|
3534
|
+
cwd: PROJECT_ROOT6,
|
|
2870
3535
|
stdio: "inherit",
|
|
2871
3536
|
shell: process.platform === "win32",
|
|
2872
3537
|
env: { ...process.env }
|
|
@@ -2921,9 +3586,9 @@ server.registerTool(
|
|
|
2921
3586
|
report += `\u2705 ${structure.testFrameworks.join(", ")} detectado(s)
|
|
2922
3587
|
`;
|
|
2923
3588
|
const testFiles = structure.testDirs.flatMap((dir) => {
|
|
2924
|
-
const fullPath =
|
|
2925
|
-
if (!
|
|
2926
|
-
return
|
|
3589
|
+
const fullPath = path6.join(PROJECT_ROOT6, dir);
|
|
3590
|
+
if (!fs6.existsSync(fullPath)) return [];
|
|
3591
|
+
return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
|
|
2927
3592
|
});
|
|
2928
3593
|
report += `\u2705 ${testFiles.length} teste(s) encontrado(s)
|
|
2929
3594
|
|
|
@@ -2934,7 +3599,7 @@ server.registerTool(
|
|
|
2934
3599
|
if (fw) {
|
|
2935
3600
|
const runResult = await new Promise((resolve) => {
|
|
2936
3601
|
const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run"], {
|
|
2937
|
-
cwd:
|
|
3602
|
+
cwd: PROJECT_ROOT6,
|
|
2938
3603
|
stdio: ["inherit", "pipe", "pipe"],
|
|
2939
3604
|
shell: process.platform === "win32"
|
|
2940
3605
|
});
|
|
@@ -3107,9 +3772,9 @@ server.registerTool(
|
|
|
3107
3772
|
const memory = loadProjectMemory();
|
|
3108
3773
|
const stats = getMemoryStats();
|
|
3109
3774
|
const testFiles = structure.testDirs.flatMap((dir) => {
|
|
3110
|
-
const fullPath =
|
|
3111
|
-
if (!
|
|
3112
|
-
return
|
|
3775
|
+
const fullPath = path6.join(PROJECT_ROOT6, dir);
|
|
3776
|
+
if (!fs6.existsSync(fullPath)) return [];
|
|
3777
|
+
return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
|
|
3113
3778
|
});
|
|
3114
3779
|
let score = 0;
|
|
3115
3780
|
const recommendations = [];
|
|
@@ -3176,9 +3841,9 @@ server.registerTool(
|
|
|
3176
3841
|
const memory = loadProjectMemory();
|
|
3177
3842
|
const suggestions = [];
|
|
3178
3843
|
const testFiles = structure.testDirs.flatMap((dir) => {
|
|
3179
|
-
const fullPath =
|
|
3180
|
-
if (!
|
|
3181
|
-
return
|
|
3844
|
+
const fullPath = path6.join(PROJECT_ROOT6, dir);
|
|
3845
|
+
if (!fs6.existsSync(fullPath)) return [];
|
|
3846
|
+
return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => f.toLowerCase());
|
|
3182
3847
|
});
|
|
3183
3848
|
const criticalFlows = ["login", "logout", "checkout", "payment", "signup", "search"];
|
|
3184
3849
|
const missingFlows = criticalFlows.filter((flow) => !testFiles.some((f) => f.includes(flow)));
|
|
@@ -3427,9 +4092,9 @@ server.registerTool(
|
|
|
3427
4092
|
const structure = detectProjectStructure();
|
|
3428
4093
|
const stats = getMemoryStats();
|
|
3429
4094
|
const testFiles = structure.testDirs.flatMap((dir) => {
|
|
3430
|
-
const fullPath =
|
|
3431
|
-
if (!
|
|
3432
|
-
return
|
|
4095
|
+
const fullPath = path6.join(PROJECT_ROOT6, dir);
|
|
4096
|
+
if (!fs6.existsSync(fullPath)) return [];
|
|
4097
|
+
return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f));
|
|
3433
4098
|
});
|
|
3434
4099
|
const industryBenchmarks = {
|
|
3435
4100
|
coverageAvg: "70-80%",
|
|
@@ -3496,16 +4161,16 @@ server.registerTool(
|
|
|
3496
4161
|
testFiles = [testFile];
|
|
3497
4162
|
} else {
|
|
3498
4163
|
testFiles = structure.testDirs.flatMap((dir) => {
|
|
3499
|
-
const fullPath =
|
|
3500
|
-
if (!
|
|
3501
|
-
return
|
|
4164
|
+
const fullPath = path6.join(PROJECT_ROOT6, dir);
|
|
4165
|
+
if (!fs6.existsSync(fullPath)) return [];
|
|
4166
|
+
return fs6.readdirSync(fullPath, { recursive: true }).filter((f) => /\.(spec|test|cy)\.(js|ts|jsx|tsx|py)$/.test(f)).map((f) => path6.join(dir, f));
|
|
3502
4167
|
});
|
|
3503
4168
|
}
|
|
3504
4169
|
const predictions = [];
|
|
3505
4170
|
for (const file of testFiles.slice(0, 20)) {
|
|
3506
|
-
const fullPath =
|
|
3507
|
-
if (!
|
|
3508
|
-
const content =
|
|
4171
|
+
const fullPath = path6.join(PROJECT_ROOT6, file);
|
|
4172
|
+
if (!fs6.existsSync(fullPath)) continue;
|
|
4173
|
+
const content = fs6.readFileSync(fullPath, "utf8");
|
|
3509
4174
|
const reasons = [];
|
|
3510
4175
|
let riskScore = 0;
|
|
3511
4176
|
if (/\.(class|id)\s*=|querySelector|\.class-name/i.test(content)) {
|
|
@@ -3580,7 +4245,7 @@ server.registerTool(
|
|
|
3580
4245
|
if (fw === "jest") {
|
|
3581
4246
|
return new Promise((resolve) => {
|
|
3582
4247
|
const child = spawn2("npx", ["jest", "--coverage"], {
|
|
3583
|
-
cwd:
|
|
4248
|
+
cwd: PROJECT_ROOT6,
|
|
3584
4249
|
stdio: ["inherit", "pipe", "pipe"],
|
|
3585
4250
|
shell: process.platform === "win32",
|
|
3586
4251
|
env: { ...process.env }
|
|
@@ -3747,15 +4412,15 @@ Framework: ${fw}`;
|
|
|
3747
4412
|
const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
|
|
3748
4413
|
const { ext, baseDir } = getExtensionAndBaseDir2(fw, structure);
|
|
3749
4414
|
const safeName = fileName + ext;
|
|
3750
|
-
testFilePath =
|
|
3751
|
-
if (!
|
|
4415
|
+
testFilePath = path6.join(baseDir, safeName);
|
|
4416
|
+
if (!fs6.existsSync(baseDir)) fs6.mkdirSync(baseDir, { recursive: true });
|
|
3752
4417
|
}
|
|
3753
|
-
|
|
4418
|
+
fs6.writeFileSync(testFilePath, testContent, "utf8");
|
|
3754
4419
|
learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
|
|
3755
4420
|
learnings.push({ attempt, action: "run_tests", result: "executando..." });
|
|
3756
4421
|
const runResult = await new Promise((resolve) => {
|
|
3757
4422
|
const child = spawn2("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
|
|
3758
|
-
cwd:
|
|
4423
|
+
cwd: PROJECT_ROOT6,
|
|
3759
4424
|
stdio: ["inherit", "pipe", "pipe"],
|
|
3760
4425
|
shell: process.platform === "win32"
|
|
3761
4426
|
});
|
|
@@ -3813,7 +4478,7 @@ ${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
|
|
|
3813
4478
|
learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
|
|
3814
4479
|
const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
|
|
3815
4480
|
testContent = fixedCode;
|
|
3816
|
-
|
|
4481
|
+
fs6.writeFileSync(testFilePath, testContent, "utf8");
|
|
3817
4482
|
learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
|
|
3818
4483
|
if (flakyAnalysis.isLikelyFlaky) {
|
|
3819
4484
|
const inferredPattern = inferFailurePattern(runResult.output, fw);
|
|
@@ -3898,15 +4563,15 @@ test.describe('${type.toUpperCase()} Test', () => {
|
|
|
3898
4563
|
async function main() {
|
|
3899
4564
|
const cmd = process.argv[2];
|
|
3900
4565
|
if (cmd === "learning-hub") {
|
|
3901
|
-
const __dirname2 =
|
|
3902
|
-
const hubPath =
|
|
4566
|
+
const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
|
|
4567
|
+
const hubPath = path6.join(__dirname2, "..", "learning-hub", "src", "server.js");
|
|
3903
4568
|
const hubUrl2 = pathToFileURL(hubPath).href;
|
|
3904
4569
|
await import(hubUrl2);
|
|
3905
4570
|
return;
|
|
3906
4571
|
}
|
|
3907
4572
|
if (cmd === "slack-bot") {
|
|
3908
|
-
const __dirname2 =
|
|
3909
|
-
const slackBotPath =
|
|
4573
|
+
const __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
|
|
4574
|
+
const slackBotPath = path6.join(__dirname2, "..", "slack-bot", "src", "index.js");
|
|
3910
4575
|
const slackBotUrl = pathToFileURL(slackBotPath).href;
|
|
3911
4576
|
await import(slackBotUrl);
|
|
3912
4577
|
return;
|