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/README.md +105 -21
- package/dist/index.js +1067 -287
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/slack-bot/README.md +4 -1
- package/slack-bot/TROUBLESHOOTING.md +41 -5
- package/slack-bot/check-config.js +11 -3
- package/slack-bot/src/config.js +22 -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) {
|
|
@@ -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 =
|
|
983
|
-
if (!
|
|
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
|
-
|
|
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",
|
|
991
|
-
cwd:
|
|
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
|
|
1038
|
-
|
|
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 =
|
|
1059
|
-
if (!
|
|
1060
|
-
return
|
|
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
|
|
1120
|
-
config({ path:
|
|
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.
|
|
1802
|
+
version: "2.1.9"
|
|
1124
1803
|
});
|
|
1125
|
-
var METRICS_FILE2 =
|
|
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 =
|
|
1147
|
-
if (!fullPath.startsWith(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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
|
-
|
|
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") ?
|
|
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") ?
|
|
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 =
|
|
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 =
|
|
2157
|
+
cwd = PROJECT_ROOT6;
|
|
1464
2158
|
} else if (selectedFramework === "mocha") {
|
|
1465
2159
|
cmd = "npx";
|
|
1466
2160
|
args = spec ? ["mocha", spec] : ["mocha"];
|
|
1467
|
-
cwd =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2175
|
+
cwd = PROJECT_ROOT6;
|
|
1481
2176
|
} else if (selectedFramework === "pytest") {
|
|
1482
2177
|
cmd = "pytest";
|
|
1483
2178
|
args = spec ? [spec] : [];
|
|
1484
|
-
cwd =
|
|
2179
|
+
cwd = PROJECT_ROOT6;
|
|
1485
2180
|
} else if (selectedFramework === "supertest" || selectedFramework === "pactum") {
|
|
1486
2181
|
cmd = "npm";
|
|
1487
2182
|
args = ["test"];
|
|
1488
|
-
cwd =
|
|
2183
|
+
cwd = PROJECT_ROOT6;
|
|
1489
2184
|
} else {
|
|
1490
2185
|
cmd = "npm";
|
|
1491
2186
|
args = ["test"];
|
|
1492
|
-
cwd =
|
|
2187
|
+
cwd = PROJECT_ROOT6;
|
|
1493
2188
|
}
|
|
1494
|
-
const
|
|
1495
|
-
|
|
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:
|
|
2195
|
+
env: runEnv
|
|
1501
2196
|
});
|
|
1502
2197
|
let stdout = "";
|
|
1503
2198
|
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
|
-
}
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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 =
|
|
1649
|
-
if (
|
|
2372
|
+
const full = path6.join(PROJECT_ROOT6, p.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2373
|
+
if (fs6.existsSync(full)) {
|
|
1650
2374
|
try {
|
|
1651
|
-
const content =
|
|
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}
|
|
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:
|
|
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 =
|
|
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 ?
|
|
1828
|
-
const filePath =
|
|
2571
|
+
const targetDir = subdir ? path6.join(baseDir, subdir) : baseDir;
|
|
2572
|
+
const filePath = path6.join(targetDir, fileName);
|
|
1829
2573
|
try {
|
|
1830
|
-
if (!
|
|
1831
|
-
|
|
2574
|
+
if (!fs6.existsSync(targetDir)) {
|
|
2575
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
1832
2576
|
}
|
|
1833
|
-
|
|
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
|
|
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 =
|
|
1985
|
-
if (
|
|
2792
|
+
const lastFailurePath = path6.join(PROJECT_ROOT6, ".qa-lab-last-failure.log");
|
|
2793
|
+
if (fs6.existsSync(lastFailurePath)) {
|
|
1986
2794
|
try {
|
|
1987
|
-
resolvedOutput =
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
2086
|
-
structuredContent: { ok: false, error:
|
|
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 =
|
|
2172
|
-
if (
|
|
2173
|
-
resolvedOutput =
|
|
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 =
|
|
2190
|
-
if (
|
|
2939
|
+
const fullPath = path6.join(PROJECT_ROOT6, testFilePath.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2940
|
+
if (fs6.existsSync(fullPath)) {
|
|
2191
2941
|
try {
|
|
2192
|
-
testCode =
|
|
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:
|
|
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 =
|
|
2302
|
-
if (
|
|
3054
|
+
const fullPath = path6.join(PROJECT_ROOT6, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
|
|
3055
|
+
if (fs6.existsSync(fullPath)) {
|
|
2303
3056
|
try {
|
|
2304
|
-
const raw =
|
|
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 =
|
|
2406
|
-
if (!fullPath.startsWith(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
3390
|
+
if (fs6.existsSync(METRICS_FILE2)) {
|
|
2638
3391
|
try {
|
|
2639
|
-
data = JSON.parse(
|
|
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 (
|
|
3429
|
+
if (fs6.existsSync(FLOWS_CONFIG_FILE)) {
|
|
2677
3430
|
try {
|
|
2678
|
-
const flowsConfig = JSON.parse(
|
|
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:
|
|
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 (
|
|
2864
|
-
else if (
|
|
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:
|
|
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 =
|
|
2925
|
-
if (!
|
|
2926
|
-
return
|
|
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:
|
|
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 =
|
|
3111
|
-
if (!
|
|
3112
|
-
return
|
|
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 =
|
|
3180
|
-
if (!
|
|
3181
|
-
return
|
|
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 =
|
|
3431
|
-
if (!
|
|
3432
|
-
return
|
|
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 =
|
|
3500
|
-
if (!
|
|
3501
|
-
return
|
|
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 =
|
|
3507
|
-
if (!
|
|
3508
|
-
const content =
|
|
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:
|
|
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 =
|
|
3751
|
-
if (!
|
|
4519
|
+
testFilePath = path6.join(baseDir, safeName);
|
|
4520
|
+
if (!fs6.existsSync(baseDir)) fs6.mkdirSync(baseDir, { recursive: true });
|
|
3752
4521
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
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",
|
|
3758
|
-
cwd:
|
|
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
|
-
|
|
3817
|
-
|
|
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 =
|
|
3902
|
-
const hubPath =
|
|
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 =
|
|
3909
|
-
const slackBotPath =
|
|
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;
|