mcp-lab-agent 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -23
- package/dist/index.js +547 -6
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -58,11 +58,33 @@ function saveProjectMemory(updates) {
|
|
|
58
58
|
if (updates.conventions) data.conventions = { ...data.conventions, ...updates.conventions };
|
|
59
59
|
if (updates.selectors) data.selectors = [.../* @__PURE__ */ new Set([...data.selectors || [], ...updates.selectors])].slice(-100);
|
|
60
60
|
if (updates.lastRun) data.lastRun = updates.lastRun;
|
|
61
|
+
if (updates.learnings) {
|
|
62
|
+
data.learnings = data.learnings || [];
|
|
63
|
+
data.learnings.push(...updates.learnings);
|
|
64
|
+
if (data.learnings.length > 200) data.learnings = data.learnings.slice(-150);
|
|
65
|
+
}
|
|
61
66
|
data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
67
|
fs.writeFileSync(MEMORY_FILE, JSON.stringify(data, null, 2), "utf8");
|
|
63
68
|
} catch {
|
|
64
69
|
}
|
|
65
70
|
}
|
|
71
|
+
function getMemoryStats() {
|
|
72
|
+
const memory = loadProjectMemory();
|
|
73
|
+
const learnings = memory.learnings || [];
|
|
74
|
+
const successfulFixes = learnings.filter((l) => l.success);
|
|
75
|
+
const selectorFixes = learnings.filter((l) => l.type === "selector_fix");
|
|
76
|
+
const timingFixes = learnings.filter((l) => l.type === "timing_fix");
|
|
77
|
+
const totalTests = learnings.filter((l) => l.type === "test_generated").length;
|
|
78
|
+
const firstAttemptSuccess = learnings.filter((l) => l.type === "test_generated" && l.passedFirstTime).length;
|
|
79
|
+
return {
|
|
80
|
+
totalLearnings: learnings.length,
|
|
81
|
+
successfulFixes: successfulFixes.length,
|
|
82
|
+
selectorFixes: selectorFixes.length,
|
|
83
|
+
timingFixes: timingFixes.length,
|
|
84
|
+
testsGenerated: totalTests,
|
|
85
|
+
firstAttemptSuccessRate: totalTests > 0 ? Math.round(firstAttemptSuccess / totalTests * 100) : 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
66
88
|
var FLAKY_PATTERNS = [
|
|
67
89
|
{ name: "timing", regex: /timeout|timed out|exceeded|wait|delay|slow|race condition/i, suggestion: "Adicione wait expl\xEDcito (ex: page.waitForSelector) ou aumente o timeout." },
|
|
68
90
|
{ name: "ordering", regex: /order|sequenc|flaky|intermittent|sometimes|random/i, suggestion: "Issole o teste ou use beforeAll/afterAll para estado limpo. Evite depend\xEAncia de ordem entre testes." },
|
|
@@ -612,12 +634,14 @@ Requisi\xE7\xF5es: ${networkRequests.length}`;
|
|
|
612
634
|
}
|
|
613
635
|
);
|
|
614
636
|
var QA_AGENTS = {
|
|
637
|
+
autonomous: { tools: ["qa_auto"], desc: "Modo aut\xF4nomo: gera, roda, corrige e aprende (loop completo)" },
|
|
615
638
|
detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
|
|
616
639
|
execution: { tools: ["run_tests", "watch_tests", "get_test_coverage"], desc: "Execu\xE7\xE3o de testes e cobertura" },
|
|
617
640
|
generation: { tools: ["generate_tests", "write_test", "create_test_template"], desc: "Gera\xE7\xE3o de testes com LLM" },
|
|
618
641
|
analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
|
|
619
642
|
browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
|
|
620
643
|
reporting: { tools: ["create_bug_report", "get_business_metrics"], desc: "Relat\xF3rios e m\xE9tricas" },
|
|
644
|
+
learning: { tools: ["qa_learning_stats"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
|
|
621
645
|
maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
|
|
622
646
|
};
|
|
623
647
|
server.registerTool(
|
|
@@ -637,6 +661,12 @@ server.registerTool(
|
|
|
637
661
|
},
|
|
638
662
|
async ({ task }) => {
|
|
639
663
|
const t = task.toLowerCase();
|
|
664
|
+
if (/autônomo|auto|completo|loop|aprende|corrige automaticamente/i.test(t)) {
|
|
665
|
+
return { content: [{ type: "text", text: "Agente: autonomous \u2192 qa_auto (loop completo: gera, roda, corrige, aprende)" }], structuredContent: { ok: true, suggestedAgent: "autonomous", suggestedTools: QA_AGENTS.autonomous.tools, description: QA_AGENTS.autonomous.desc } };
|
|
666
|
+
}
|
|
667
|
+
if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats/i.test(t)) {
|
|
668
|
+
return { content: [{ type: "text", text: "Agente: learning \u2192 qa_learning_stats" }], structuredContent: { ok: true, suggestedAgent: "learning", suggestedTools: QA_AGENTS.learning.tools, description: QA_AGENTS.learning.desc } };
|
|
669
|
+
}
|
|
640
670
|
if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
|
|
641
671
|
return { content: [{ type: "text", text: "Agente: execution \u2192 run_tests, get_test_coverage" }], structuredContent: { ok: true, suggestedAgent: "execution", suggestedTools: QA_AGENTS.execution.tools, description: QA_AGENTS.execution.desc } };
|
|
642
672
|
}
|
|
@@ -1225,6 +1255,80 @@ async function callLlmForExplanation(provider, apiKey, baseUrl, model, systemPro
|
|
|
1225
1255
|
const data = await res.json();
|
|
1226
1256
|
return data.choices?.[0]?.message?.content || "";
|
|
1227
1257
|
}
|
|
1258
|
+
async function generateFailureExplanation(resolvedOutput, testFilePath = null) {
|
|
1259
|
+
const structure = detectProjectStructure();
|
|
1260
|
+
const fw = structure.testFrameworks[0] || "unknown";
|
|
1261
|
+
let testCode = "";
|
|
1262
|
+
if (testFilePath) {
|
|
1263
|
+
const normalized = testFilePath.replace(/^\//, "").replace(/\\/g, "/");
|
|
1264
|
+
const fullPath = path.join(PROJECT_ROOT, normalized);
|
|
1265
|
+
if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) {
|
|
1266
|
+
try {
|
|
1267
|
+
testCode = fs.readFileSync(fullPath, "utf8");
|
|
1268
|
+
} catch {
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const llm = resolveLLMProvider("complex");
|
|
1273
|
+
if (!llm.apiKey) {
|
|
1274
|
+
return { ok: false, error: "No API key", formattedText: null };
|
|
1275
|
+
}
|
|
1276
|
+
const { provider, apiKey, baseUrl, model } = llm;
|
|
1277
|
+
const fwHints = {
|
|
1278
|
+
webdriverio: "WebdriverIO (describe/it, $, browser.$)",
|
|
1279
|
+
appium: "Appium/WebdriverIO (mobile, $, browser.$)",
|
|
1280
|
+
playwright: "Playwright (test, page, locator)",
|
|
1281
|
+
cypress: "Cypress (cy.get, cy.click)",
|
|
1282
|
+
jest: "Jest (describe, test, expect)",
|
|
1283
|
+
vitest: "Vitest (describe, test, expect)",
|
|
1284
|
+
robot: "Robot Framework",
|
|
1285
|
+
pytest: "pytest"
|
|
1286
|
+
};
|
|
1287
|
+
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:
|
|
1288
|
+
- oQueAconteceu: string (explica\xE7\xE3o em portugu\xEAs do que aconteceu, simples)
|
|
1289
|
+
- porQueProvavelmenteFalhou: array de strings (lista de poss\xEDveis causas, uma por item)
|
|
1290
|
+
- oQueFazerAgora: array de strings (passos numerados do que fazer)
|
|
1291
|
+
- sugestaoCorrecao: string ou null (c\xF3digo de corre\xE7\xE3o se aplic\xE1vel, no formato do framework)
|
|
1292
|
+
- conceito: string ou null (ex: "Flaky test = teste intermitente. Geralmente por timing ou seletores fr\xE1geis.")
|
|
1293
|
+
- framework: string (framework do projeto)
|
|
1294
|
+
|
|
1295
|
+
Framework do projeto: ${fw}. ${fwHints[fw] || ""}
|
|
1296
|
+
Responda APENAS com o JSON v\xE1lido, sem texto antes ou depois.`;
|
|
1297
|
+
const userPrompt = `Output do terminal/log (teste falhou):
|
|
1298
|
+
---
|
|
1299
|
+
${resolvedOutput.slice(0, 12e3)}
|
|
1300
|
+
---
|
|
1301
|
+
${testCode ? `
|
|
1302
|
+
C\xF3digo do teste que falhou:
|
|
1303
|
+
---
|
|
1304
|
+
${testCode.slice(0, 6e3)}
|
|
1305
|
+
---` : ""}`;
|
|
1306
|
+
try {
|
|
1307
|
+
let raw = await callLlmForExplanation(provider, apiKey, baseUrl, model, systemPrompt, userPrompt);
|
|
1308
|
+
raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
|
|
1309
|
+
let data = {};
|
|
1310
|
+
try {
|
|
1311
|
+
data = JSON.parse(raw);
|
|
1312
|
+
} catch {
|
|
1313
|
+
data = {
|
|
1314
|
+
oQueAconteceu: raw.slice(0, 500) || "N\xE3o foi poss\xEDvel parsear a resposta.",
|
|
1315
|
+
porQueProvavelmenteFalhou: [],
|
|
1316
|
+
oQueFazerAgora: [],
|
|
1317
|
+
sugestaoCorrecao: null,
|
|
1318
|
+
conceito: null,
|
|
1319
|
+
framework: fw
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
data.framework = data.framework || fw;
|
|
1323
|
+
if (testFilePath && data.sugestaoCorrecao) {
|
|
1324
|
+
saveProjectMemory({ patterns: { [testFilePath]: { lastFix: data.sugestaoCorrecao?.slice(0, 500) } } });
|
|
1325
|
+
}
|
|
1326
|
+
const formattedText = formatFailureExplanation(data);
|
|
1327
|
+
return { ok: true, formattedText, structuredContent: data };
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
return { ok: false, error: err.message, formattedText: null };
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1228
1332
|
server.registerTool(
|
|
1229
1333
|
"por_que_falhou",
|
|
1230
1334
|
{
|
|
@@ -2047,6 +2151,39 @@ server.registerTool(
|
|
|
2047
2151
|
});
|
|
2048
2152
|
}
|
|
2049
2153
|
);
|
|
2154
|
+
server.registerTool(
|
|
2155
|
+
"qa_learning_stats",
|
|
2156
|
+
{
|
|
2157
|
+
title: "Estat\xEDsticas de aprendizado",
|
|
2158
|
+
description: "[M\xC9TRICAS] Retorna m\xE9tricas de aprendizado do agente: quantos testes gerados, taxa de sucesso na primeira tentativa, corre\xE7\xF5es aplicadas, etc.",
|
|
2159
|
+
inputSchema: z.object({}),
|
|
2160
|
+
outputSchema: z.object({
|
|
2161
|
+
totalLearnings: z.number(),
|
|
2162
|
+
successfulFixes: z.number(),
|
|
2163
|
+
selectorFixes: z.number(),
|
|
2164
|
+
timingFixes: z.number(),
|
|
2165
|
+
testsGenerated: z.number(),
|
|
2166
|
+
firstAttemptSuccessRate: z.number()
|
|
2167
|
+
})
|
|
2168
|
+
},
|
|
2169
|
+
async () => {
|
|
2170
|
+
const stats = getMemoryStats();
|
|
2171
|
+
const summary = `\u{1F4CA} **Estat\xEDsticas de Aprendizado**
|
|
2172
|
+
|
|
2173
|
+
- Total de aprendizados: ${stats.totalLearnings}
|
|
2174
|
+
- Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
|
|
2175
|
+
- Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
|
|
2176
|
+
- Corre\xE7\xF5es de timing: ${stats.timingFixes}
|
|
2177
|
+
- Testes gerados: ${stats.testsGenerated}
|
|
2178
|
+
- Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
|
|
2179
|
+
|
|
2180
|
+
${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Use qa_auto para gerar testes e aprender com erros." : ""}`;
|
|
2181
|
+
return {
|
|
2182
|
+
content: [{ type: "text", text: summary }],
|
|
2183
|
+
structuredContent: stats
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
);
|
|
2050
2187
|
server.registerTool(
|
|
2051
2188
|
"get_test_coverage",
|
|
2052
2189
|
{
|
|
@@ -2129,6 +2266,198 @@ server.registerTool(
|
|
|
2129
2266
|
};
|
|
2130
2267
|
}
|
|
2131
2268
|
);
|
|
2269
|
+
server.registerTool(
|
|
2270
|
+
"qa_auto",
|
|
2271
|
+
{
|
|
2272
|
+
title: "Modo aut\xF4nomo: gera, roda, corrige e aprende",
|
|
2273
|
+
description: "[AGENTE AUT\xD4NOMO] Loop completo: detecta projeto \u2192 gera teste \u2192 roda \u2192 se falhar: analisa, corrige, roda de novo \u2192 aprende com erros. Repete at\xE9 passar ou atingir max_retries.",
|
|
2274
|
+
inputSchema: z.object({
|
|
2275
|
+
request: z.string().describe("O que testar (ex: 'login flow', 'checkout', 'API /users')."),
|
|
2276
|
+
framework: z.enum([
|
|
2277
|
+
"cypress",
|
|
2278
|
+
"playwright",
|
|
2279
|
+
"webdriverio",
|
|
2280
|
+
"jest",
|
|
2281
|
+
"vitest",
|
|
2282
|
+
"mocha",
|
|
2283
|
+
"appium",
|
|
2284
|
+
"robot",
|
|
2285
|
+
"pytest"
|
|
2286
|
+
]).optional().describe("Framework (detectado automaticamente se omitido)."),
|
|
2287
|
+
maxRetries: z.number().optional().describe("M\xE1ximo de tentativas de corre\xE7\xE3o. Default: 3.")
|
|
2288
|
+
}),
|
|
2289
|
+
outputSchema: z.object({
|
|
2290
|
+
ok: z.boolean(),
|
|
2291
|
+
testFilePath: z.string().optional(),
|
|
2292
|
+
attempts: z.number(),
|
|
2293
|
+
finalStatus: z.enum(["passed", "failed", "max_retries"]),
|
|
2294
|
+
learnings: z.array(z.object({ attempt: z.number(), action: z.string(), result: z.string() })).optional(),
|
|
2295
|
+
error: z.string().optional()
|
|
2296
|
+
})
|
|
2297
|
+
},
|
|
2298
|
+
async ({ request, framework, maxRetries = 3 }) => {
|
|
2299
|
+
const structure = detectProjectStructure();
|
|
2300
|
+
const fw = framework || structure.testFrameworks[0];
|
|
2301
|
+
if (!fw) {
|
|
2302
|
+
return {
|
|
2303
|
+
content: [{ type: "text", text: "Nenhum framework detectado. Configure testes primeiro." }],
|
|
2304
|
+
structuredContent: { ok: false, error: "No framework", finalStatus: "failed", attempts: 0 }
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
const llm = resolveLLMProvider("simple");
|
|
2308
|
+
if (!llm.apiKey) {
|
|
2309
|
+
return {
|
|
2310
|
+
content: [{ type: "text", text: "Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env" }],
|
|
2311
|
+
structuredContent: { ok: false, error: "No API key", finalStatus: "failed", attempts: 0 }
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
const learnings = [];
|
|
2315
|
+
const memory = loadProjectMemory();
|
|
2316
|
+
const contextLines = [
|
|
2317
|
+
`Frameworks: ${structure.testFrameworks.join(", ")}`,
|
|
2318
|
+
`Pastas: ${structure.testDirs.join(", ")}`,
|
|
2319
|
+
memory.flows?.length ? `Fluxos: ${memory.flows.map((f) => f.name || f.id).join(", ")}` : ""
|
|
2320
|
+
].filter(Boolean).join("\n");
|
|
2321
|
+
let testFilePath = null;
|
|
2322
|
+
let testContent = null;
|
|
2323
|
+
let attempt = 0;
|
|
2324
|
+
learnings.push({ attempt: 0, action: "detect_project", result: `${structure.testFrameworks.length} framework(s)` });
|
|
2325
|
+
for (attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2326
|
+
learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
|
|
2327
|
+
const { provider, apiKey, baseUrl, model } = llm;
|
|
2328
|
+
const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
|
|
2329
|
+
const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
|
|
2330
|
+
${memoryHints ? `
|
|
2331
|
+
Aprendizados anteriores (use como refer\xEAncia):
|
|
2332
|
+
${memoryHints.slice(0, 1e3)}` : ""}
|
|
2333
|
+
Retorne SOMENTE o c\xF3digo, sem markdown.`;
|
|
2334
|
+
const userPrompt = `Contexto:
|
|
2335
|
+
${contextLines}
|
|
2336
|
+
|
|
2337
|
+
Gere teste para: ${request}
|
|
2338
|
+
Framework: ${fw}`;
|
|
2339
|
+
try {
|
|
2340
|
+
let specContent = "";
|
|
2341
|
+
if (provider === "gemini") {
|
|
2342
|
+
const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
|
|
2343
|
+
const res = await fetch(url, {
|
|
2344
|
+
method: "POST",
|
|
2345
|
+
headers: { "Content-Type": "application/json" },
|
|
2346
|
+
body: JSON.stringify({
|
|
2347
|
+
contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
|
|
2348
|
+
generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
|
|
2349
|
+
})
|
|
2350
|
+
});
|
|
2351
|
+
const data = await res.json();
|
|
2352
|
+
specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
2353
|
+
} else {
|
|
2354
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2355
|
+
method: "POST",
|
|
2356
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
2357
|
+
body: JSON.stringify({
|
|
2358
|
+
model,
|
|
2359
|
+
messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
|
|
2360
|
+
temperature: 0.3,
|
|
2361
|
+
max_tokens: 4096
|
|
2362
|
+
})
|
|
2363
|
+
});
|
|
2364
|
+
const data = await res.json();
|
|
2365
|
+
specContent = data.choices?.[0]?.message?.content || "";
|
|
2366
|
+
}
|
|
2367
|
+
specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
2368
|
+
testContent = specContent;
|
|
2369
|
+
if (!testFilePath) {
|
|
2370
|
+
const fileName = request.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
|
|
2371
|
+
const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
|
|
2372
|
+
const safeName = fileName + ext;
|
|
2373
|
+
testFilePath = path.join(baseDir, safeName);
|
|
2374
|
+
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
|
|
2375
|
+
}
|
|
2376
|
+
fs.writeFileSync(testFilePath, testContent, "utf8");
|
|
2377
|
+
learnings.push({ attempt, action: "write_test", result: `gravado: ${testFilePath}` });
|
|
2378
|
+
learnings.push({ attempt, action: "run_tests", result: "executando..." });
|
|
2379
|
+
const runResult = await new Promise((resolve) => {
|
|
2380
|
+
const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
|
|
2381
|
+
cwd: PROJECT_ROOT,
|
|
2382
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
2383
|
+
shell: process.platform === "win32"
|
|
2384
|
+
});
|
|
2385
|
+
let stdout = "", stderr = "";
|
|
2386
|
+
if (child.stdout) child.stdout.on("data", (d) => {
|
|
2387
|
+
stdout += d.toString();
|
|
2388
|
+
});
|
|
2389
|
+
if (child.stderr) child.stderr.on("data", (d) => {
|
|
2390
|
+
stderr += d.toString();
|
|
2391
|
+
});
|
|
2392
|
+
child.on("close", (code) => resolve({ code, output: [stdout, stderr].filter(Boolean).join("\n") }));
|
|
2393
|
+
});
|
|
2394
|
+
if (runResult.code === 0) {
|
|
2395
|
+
learnings.push({ attempt, action: "run_tests", result: "\u2705 passou" });
|
|
2396
|
+
saveProjectMemory({
|
|
2397
|
+
learnings: [{ type: "test_generated", request, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
2398
|
+
});
|
|
2399
|
+
return {
|
|
2400
|
+
content: [{ type: "text", text: `\u2705 Teste passou na tentativa ${attempt}!
|
|
2401
|
+
|
|
2402
|
+
Arquivo: ${testFilePath}
|
|
2403
|
+
|
|
2404
|
+
Aprendizados salvos.` }],
|
|
2405
|
+
structuredContent: { ok: true, testFilePath, attempts: attempt, finalStatus: "passed", learnings }
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
learnings.push({ attempt, action: "run_tests", result: `\u274C falhou (exit ${runResult.code})` });
|
|
2409
|
+
if (attempt >= maxRetries) {
|
|
2410
|
+
learnings.push({ attempt, action: "max_retries", result: "limite atingido" });
|
|
2411
|
+
saveProjectMemory({
|
|
2412
|
+
learnings: [{ type: "test_generated", request, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
2413
|
+
});
|
|
2414
|
+
return {
|
|
2415
|
+
content: [{ type: "text", text: `\u274C Teste falhou ap\xF3s ${attempt} tentativa(s).
|
|
2416
|
+
|
|
2417
|
+
\xDAltimo erro:
|
|
2418
|
+
${runResult.output.slice(0, 500)}` }],
|
|
2419
|
+
structuredContent: { ok: false, testFilePath, attempts: attempt, finalStatus: "max_retries", learnings }
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
learnings.push({ attempt, action: "analyze_failures", result: "analisando..." });
|
|
2423
|
+
const flakyAnalysis = detectFlakyPatterns(runResult.output);
|
|
2424
|
+
const llmComplex = resolveLLMProvider("complex");
|
|
2425
|
+
const explainResult = await generateFailureExplanation(runResult.output, testFilePath);
|
|
2426
|
+
if (!explainResult.ok || !explainResult.structuredContent?.sugestaoCorrecao) {
|
|
2427
|
+
learnings.push({ attempt, action: "analyze_failures", result: "sem sugest\xE3o de corre\xE7\xE3o" });
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
learnings.push({ attempt, action: "apply_fix", result: "aplicando corre\xE7\xE3o..." });
|
|
2431
|
+
const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
|
|
2432
|
+
testContent = fixedCode;
|
|
2433
|
+
fs.writeFileSync(testFilePath, testContent, "utf8");
|
|
2434
|
+
learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
|
|
2435
|
+
if (flakyAnalysis.isLikelyFlaky) {
|
|
2436
|
+
saveProjectMemory({
|
|
2437
|
+
learnings: [{
|
|
2438
|
+
type: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
|
|
2439
|
+
request,
|
|
2440
|
+
framework: fw,
|
|
2441
|
+
fix: fixedCode.slice(0, 500),
|
|
2442
|
+
success: false,
|
|
2443
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2444
|
+
}]
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
learnings.push({ attempt, action: "error", result: err.message });
|
|
2449
|
+
return {
|
|
2450
|
+
content: [{ type: "text", text: `Erro na tentativa ${attempt}: ${err.message}` }],
|
|
2451
|
+
structuredContent: { ok: false, error: err.message, attempts: attempt, finalStatus: "failed", learnings }
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return {
|
|
2456
|
+
content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).` }],
|
|
2457
|
+
structuredContent: { ok: false, testFilePath, attempts: maxRetries, finalStatus: "max_retries", learnings }
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
);
|
|
2132
2461
|
server.registerTool(
|
|
2133
2462
|
"create_test_template",
|
|
2134
2463
|
{
|
|
@@ -2179,16 +2508,23 @@ async function main() {
|
|
|
2179
2508
|
const cmd = process.argv[2];
|
|
2180
2509
|
if (cmd === "--help" || cmd === "-h") {
|
|
2181
2510
|
console.log(`
|
|
2182
|
-
mcp-lab-agent -
|
|
2511
|
+
mcp-lab-agent - Agente aut\xF4nomo de QA que aprende com os pr\xF3prios erros
|
|
2183
2512
|
|
|
2184
2513
|
USO:
|
|
2185
2514
|
mcp-lab-agent [comando] # Sem comando: inicia servidor MCP
|
|
2186
2515
|
mcp-lab-agent --help # Mostra esta ajuda
|
|
2187
2516
|
|
|
2188
2517
|
COMANDOS CLI:
|
|
2189
|
-
detect
|
|
2190
|
-
route <tarefa>
|
|
2191
|
-
list
|
|
2518
|
+
detect [--json] Detecta frameworks e estrutura. Padr\xE3o: resumo. --json: JSON completo para scripts.
|
|
2519
|
+
route <tarefa> Sugere qual ferramenta usar (ex: route "rodar testes")
|
|
2520
|
+
list Lista ferramentas MCP dispon\xEDveis
|
|
2521
|
+
auto <descri\xE7\xE3o> [--max-retries N] [NOVO] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
|
|
2522
|
+
stats [NOVO] Mostra estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
|
|
2523
|
+
|
|
2524
|
+
EXEMPLOS:
|
|
2525
|
+
mcp-lab-agent auto "login flow" --max-retries 5
|
|
2526
|
+
mcp-lab-agent stats
|
|
2527
|
+
mcp-lab-agent detect --json
|
|
2192
2528
|
|
|
2193
2529
|
INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
|
|
2194
2530
|
Adicione ao ~/.cursor/mcp.json:
|
|
@@ -2206,7 +2542,25 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
|
|
|
2206
2542
|
}
|
|
2207
2543
|
if (cmd === "detect") {
|
|
2208
2544
|
const structure = detectProjectStructure();
|
|
2209
|
-
|
|
2545
|
+
const jsonOnly = process.argv.includes("--json");
|
|
2546
|
+
if (jsonOnly) {
|
|
2547
|
+
console.log(JSON.stringify(structure, null, 2));
|
|
2548
|
+
} else {
|
|
2549
|
+
const lines = [
|
|
2550
|
+
"",
|
|
2551
|
+
"mcp-lab-agent \xB7 detec\xE7\xE3o",
|
|
2552
|
+
"\u2500".repeat(40),
|
|
2553
|
+
`Frameworks: ${structure.testFrameworks.length ? structure.testFrameworks.join(", ") : "nenhum"}`,
|
|
2554
|
+
`Pastas: ${structure.testDirs.length ? structure.testDirs.join(", ") : "nenhuma"}`,
|
|
2555
|
+
`Backend: ${structure.backendDir || "\u2014"}`,
|
|
2556
|
+
`Frontend: ${structure.frontendDir || "\u2014"}`,
|
|
2557
|
+
`Mobile: ${structure.hasMobile ? "sim" : "\u2014"}`,
|
|
2558
|
+
"\u2500".repeat(40),
|
|
2559
|
+
"(use --json para sa\xEDda completa)",
|
|
2560
|
+
""
|
|
2561
|
+
];
|
|
2562
|
+
console.log(lines.join("\n"));
|
|
2563
|
+
}
|
|
2210
2564
|
process.exit(0);
|
|
2211
2565
|
}
|
|
2212
2566
|
if (cmd === "list") {
|
|
@@ -2218,7 +2572,9 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
|
|
|
2218
2572
|
const task = process.argv.slice(3).join(" ");
|
|
2219
2573
|
const t = task.toLowerCase();
|
|
2220
2574
|
let agent = "detection";
|
|
2221
|
-
if (/
|
|
2575
|
+
if (/autônomo|auto|completo|loop|aprende|corrige automaticamente/i.test(t)) agent = "autonomous";
|
|
2576
|
+
else if (/estatística|métrica de aprendizado|taxa de sucesso|learning|stats/i.test(t)) agent = "learning";
|
|
2577
|
+
else if (/rodar|executar|run|test|coverage|watch/i.test(t)) agent = "execution";
|
|
2222
2578
|
else if (/gerar|criar|escrever|generate|write|template/i.test(t)) agent = "generation";
|
|
2223
2579
|
else if (/analisar|por que|falhou|sugerir|fix|selector/i.test(t)) agent = "analysis";
|
|
2224
2580
|
else if (/browser|navegador|screenshot|network|console/i.test(t)) agent = "browser";
|
|
@@ -2228,6 +2584,191 @@ INTEGRA\xC7\xC3O MCP (Cursor/Cline/Windsurf):
|
|
|
2228
2584
|
console.log(JSON.stringify({ suggestedAgent: agent, suggestedTools: a.tools, description: a.desc }, null, 2));
|
|
2229
2585
|
process.exit(0);
|
|
2230
2586
|
}
|
|
2587
|
+
if (cmd === "auto") {
|
|
2588
|
+
const request = process.argv.slice(3).join(" ");
|
|
2589
|
+
if (!request) {
|
|
2590
|
+
console.error("\u274C Uso: mcp-lab-agent auto <descri\xE7\xE3o do teste> [--max-retries N]");
|
|
2591
|
+
process.exit(1);
|
|
2592
|
+
}
|
|
2593
|
+
const maxRetriesIdx = process.argv.indexOf("--max-retries");
|
|
2594
|
+
const maxRetries = maxRetriesIdx !== -1 && process.argv[maxRetriesIdx + 1] ? parseInt(process.argv[maxRetriesIdx + 1], 10) : 3;
|
|
2595
|
+
const cleanRequest = request.replace(/--max-retries\s+\d+/g, "").trim();
|
|
2596
|
+
console.log(`
|
|
2597
|
+
\u{1F916} Modo aut\xF4nomo iniciado: "${cleanRequest}"
|
|
2598
|
+
`);
|
|
2599
|
+
const structure = detectProjectStructure();
|
|
2600
|
+
const fw = structure.testFrameworks[0];
|
|
2601
|
+
if (!fw) {
|
|
2602
|
+
console.error("\u274C Nenhum framework detectado.");
|
|
2603
|
+
process.exit(1);
|
|
2604
|
+
}
|
|
2605
|
+
const llm = resolveLLMProvider("simple");
|
|
2606
|
+
if (!llm.apiKey) {
|
|
2607
|
+
console.error("\u274C Configure GROQ_API_KEY, GEMINI_API_KEY ou OPENAI_API_KEY no .env");
|
|
2608
|
+
process.exit(1);
|
|
2609
|
+
}
|
|
2610
|
+
const memory = loadProjectMemory();
|
|
2611
|
+
const contextLines = [
|
|
2612
|
+
`Frameworks: ${structure.testFrameworks.join(", ")}`,
|
|
2613
|
+
`Pastas: ${structure.testDirs.join(", ")}`,
|
|
2614
|
+
memory.flows?.length ? `Fluxos: ${memory.flows.map((f) => f.name || f.id).join(", ")}` : ""
|
|
2615
|
+
].filter(Boolean).join("\n");
|
|
2616
|
+
let testFilePath = null;
|
|
2617
|
+
let testContent = null;
|
|
2618
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2619
|
+
console.log(`
|
|
2620
|
+
[Tentativa ${attempt}/${maxRetries}] Gerando teste...`);
|
|
2621
|
+
const { provider, apiKey, baseUrl, model } = llm;
|
|
2622
|
+
const memoryHints = memory.learnings?.filter((l) => l.success).slice(-10).map((l) => l.fix).join("\n") || "";
|
|
2623
|
+
const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
|
|
2624
|
+
${memoryHints ? `
|
|
2625
|
+
Aprendizados anteriores (use como refer\xEAncia):
|
|
2626
|
+
${memoryHints.slice(0, 1e3)}` : ""}
|
|
2627
|
+
Retorne SOMENTE o c\xF3digo, sem markdown.`;
|
|
2628
|
+
const userPrompt = `Contexto:
|
|
2629
|
+
${contextLines}
|
|
2630
|
+
|
|
2631
|
+
Gere teste para: ${cleanRequest}
|
|
2632
|
+
Framework: ${fw}`;
|
|
2633
|
+
try {
|
|
2634
|
+
let specContent = "";
|
|
2635
|
+
if (provider === "gemini") {
|
|
2636
|
+
const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
|
|
2637
|
+
const res = await fetch(url, {
|
|
2638
|
+
method: "POST",
|
|
2639
|
+
headers: { "Content-Type": "application/json" },
|
|
2640
|
+
body: JSON.stringify({
|
|
2641
|
+
contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
|
|
2642
|
+
generationConfig: { temperature: 0.3, maxOutputTokens: 4096 }
|
|
2643
|
+
})
|
|
2644
|
+
});
|
|
2645
|
+
const data = await res.json();
|
|
2646
|
+
specContent = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
2647
|
+
} else {
|
|
2648
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2649
|
+
method: "POST",
|
|
2650
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
2651
|
+
body: JSON.stringify({
|
|
2652
|
+
model,
|
|
2653
|
+
messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
|
|
2654
|
+
temperature: 0.3,
|
|
2655
|
+
max_tokens: 4096
|
|
2656
|
+
})
|
|
2657
|
+
});
|
|
2658
|
+
const data = await res.json();
|
|
2659
|
+
specContent = data.choices?.[0]?.message?.content || "";
|
|
2660
|
+
}
|
|
2661
|
+
specContent = specContent.replace(/^```(?:js|javascript|typescript)?\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
2662
|
+
testContent = specContent;
|
|
2663
|
+
if (!testFilePath) {
|
|
2664
|
+
const fileName = cleanRequest.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
|
|
2665
|
+
const { ext, baseDir } = getExtensionAndBaseDir(fw, structure);
|
|
2666
|
+
const safeName = fileName + ext;
|
|
2667
|
+
testFilePath = path.join(baseDir, safeName);
|
|
2668
|
+
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
|
|
2669
|
+
}
|
|
2670
|
+
fs.writeFileSync(testFilePath, testContent, "utf8");
|
|
2671
|
+
console.log(`\u2705 Teste gravado: ${testFilePath}`);
|
|
2672
|
+
console.log(`
|
|
2673
|
+
[Tentativa ${attempt}/${maxRetries}] Executando teste...`);
|
|
2674
|
+
const runResult = await new Promise((resolve) => {
|
|
2675
|
+
const child = spawn("npx", [fw === "cypress" ? "cypress" : fw === "playwright" ? "playwright" : fw, fw === "cypress" ? "run" : fw === "playwright" ? "test" : "run", testFilePath], {
|
|
2676
|
+
cwd: PROJECT_ROOT,
|
|
2677
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
2678
|
+
shell: process.platform === "win32"
|
|
2679
|
+
});
|
|
2680
|
+
let stdout = "", stderr = "";
|
|
2681
|
+
if (child.stdout) child.stdout.on("data", (d) => {
|
|
2682
|
+
stdout += d.toString();
|
|
2683
|
+
});
|
|
2684
|
+
if (child.stderr) child.stderr.on("data", (d) => {
|
|
2685
|
+
stderr += d.toString();
|
|
2686
|
+
});
|
|
2687
|
+
child.on("close", (code) => resolve({ code, output: [stdout, stderr].filter(Boolean).join("\n") }));
|
|
2688
|
+
});
|
|
2689
|
+
if (runResult.code === 0) {
|
|
2690
|
+
console.log(`
|
|
2691
|
+
\u2705 Teste passou na tentativa ${attempt}!`);
|
|
2692
|
+
saveProjectMemory({
|
|
2693
|
+
learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
2694
|
+
});
|
|
2695
|
+
console.log(`
|
|
2696
|
+
\u{1F4CA} Aprendizado salvo. Use "mcp-lab-agent stats" para ver m\xE9tricas.
|
|
2697
|
+
`);
|
|
2698
|
+
process.exit(0);
|
|
2699
|
+
}
|
|
2700
|
+
console.log(`
|
|
2701
|
+
\u274C Teste falhou (exit ${runResult.code})`);
|
|
2702
|
+
console.log(`
|
|
2703
|
+
Sa\xEDda:
|
|
2704
|
+
${runResult.output.slice(0, 800)}
|
|
2705
|
+
`);
|
|
2706
|
+
if (attempt >= maxRetries) {
|
|
2707
|
+
console.log(`
|
|
2708
|
+
\u274C Limite de tentativas atingido (${maxRetries}).
|
|
2709
|
+
`);
|
|
2710
|
+
saveProjectMemory({
|
|
2711
|
+
learnings: [{ type: "test_generated", request: cleanRequest, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
2712
|
+
});
|
|
2713
|
+
process.exit(1);
|
|
2714
|
+
}
|
|
2715
|
+
console.log(`
|
|
2716
|
+
[Tentativa ${attempt}/${maxRetries}] Analisando falha...`);
|
|
2717
|
+
const flakyAnalysis = detectFlakyPatterns(runResult.output);
|
|
2718
|
+
if (flakyAnalysis.isLikelyFlaky) {
|
|
2719
|
+
console.log(`\u26A0\uFE0F Flaky detectado (${flakyAnalysis.confidence.toFixed(2)}): ${flakyAnalysis.patterns.map((p) => p.pattern).join(", ")}`);
|
|
2720
|
+
}
|
|
2721
|
+
const explainResult = await generateFailureExplanation(runResult.output, testFilePath);
|
|
2722
|
+
if (!explainResult.ok || !explainResult.structuredContent?.sugestaoCorrecao) {
|
|
2723
|
+
console.log(`\u26A0\uFE0F N\xE3o foi poss\xEDvel gerar corre\xE7\xE3o. Tentando novamente...`);
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
console.log(`
|
|
2727
|
+
[Tentativa ${attempt}/${maxRetries}] Aplicando corre\xE7\xE3o...`);
|
|
2728
|
+
const fixedCode = explainResult.structuredContent.sugestaoCorrecao;
|
|
2729
|
+
testContent = fixedCode;
|
|
2730
|
+
fs.writeFileSync(testFilePath, testContent, "utf8");
|
|
2731
|
+
console.log(`\u2705 Corre\xE7\xE3o aplicada.`);
|
|
2732
|
+
if (flakyAnalysis.isLikelyFlaky) {
|
|
2733
|
+
saveProjectMemory({
|
|
2734
|
+
learnings: [{
|
|
2735
|
+
type: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
|
|
2736
|
+
request: cleanRequest,
|
|
2737
|
+
framework: fw,
|
|
2738
|
+
fix: fixedCode.slice(0, 500),
|
|
2739
|
+
success: false,
|
|
2740
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2741
|
+
}]
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
console.error(`
|
|
2746
|
+
\u274C Erro na tentativa ${attempt}: ${err.message}
|
|
2747
|
+
`);
|
|
2748
|
+
process.exit(1);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
console.log(`
|
|
2752
|
+
\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).
|
|
2753
|
+
`);
|
|
2754
|
+
process.exit(1);
|
|
2755
|
+
}
|
|
2756
|
+
if (cmd === "stats") {
|
|
2757
|
+
const stats = getMemoryStats();
|
|
2758
|
+
console.log(`
|
|
2759
|
+
\u{1F4CA} Estat\xEDsticas de Aprendizado
|
|
2760
|
+
|
|
2761
|
+
Total de aprendizados: ${stats.totalLearnings}
|
|
2762
|
+
Corre\xE7\xF5es bem-sucedidas: ${stats.successfulFixes}
|
|
2763
|
+
Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
|
|
2764
|
+
Corre\xE7\xF5es de timing: ${stats.timingFixes}
|
|
2765
|
+
Testes gerados: ${stats.testsGenerated}
|
|
2766
|
+
Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
|
|
2767
|
+
|
|
2768
|
+
${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." : ""}
|
|
2769
|
+
`);
|
|
2770
|
+
process.exit(0);
|
|
2771
|
+
}
|
|
2231
2772
|
const transport = new StdioServerTransport();
|
|
2232
2773
|
await server.connect(transport);
|
|
2233
2774
|
}
|