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