mcp-lab-agent 2.1.3 → 2.1.6
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 +187 -227
- package/dist/index.js +447 -19
- 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 +7 -3
- package/slack-bot/.env.example +17 -2
- package/slack-bot/CREDENTIALS.md +23 -0
- package/slack-bot/README.md +87 -16
- package/slack-bot/TROUBLESHOOTING.md +73 -0
- package/slack-bot/check-config.js +117 -0
- package/slack-bot/package.json +2 -1
- package/slack-bot/setup.js +14 -8
- package/slack-bot/src/config.js +25 -9
- package/slack-bot/src/index.js +46 -12
- package/slack-bot/src/workers/qa-job.js +17 -10
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
import { spawn as spawn2 } from "child_process";
|
|
9
9
|
import path5 from "path";
|
|
10
10
|
import fs5 from "fs";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
11
12
|
|
|
12
13
|
// src/core/llm-router.js
|
|
13
14
|
function resolveLLMProvider(taskType = "simple") {
|
|
@@ -41,6 +42,44 @@ function resolveLLMProvider(taskType = "simple") {
|
|
|
41
42
|
// src/core/memory.js
|
|
42
43
|
import path from "path";
|
|
43
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
|
|
44
83
|
var PROJECT_ROOT = process.cwd();
|
|
45
84
|
var MEMORY_FILE = path.join(PROJECT_ROOT, ".qa-lab-memory.json");
|
|
46
85
|
var FLOWS_CONFIG_FILE2 = path.join(PROJECT_ROOT, "qa-lab-flows.json");
|
|
@@ -73,6 +112,8 @@ function saveProjectMemory(updates) {
|
|
|
73
112
|
data.learnings = data.learnings || [];
|
|
74
113
|
data.learnings.push(...updates.learnings);
|
|
75
114
|
if (data.learnings.length > 200) data.learnings = data.learnings.slice(-150);
|
|
115
|
+
syncLearningsToHub(updates.learnings).catch(() => {
|
|
116
|
+
});
|
|
76
117
|
}
|
|
77
118
|
if (updates.execution) {
|
|
78
119
|
data.executions = data.executions || [];
|
|
@@ -84,12 +125,17 @@ function saveProjectMemory(updates) {
|
|
|
84
125
|
} catch {
|
|
85
126
|
}
|
|
86
127
|
}
|
|
128
|
+
var LEARNING_TYPES = ["selector_fix", "timing_fix", "element_not_rendered", "element_not_visible", "element_stale", "mobile_mapping_invisible"];
|
|
87
129
|
function getMemoryStats() {
|
|
88
130
|
const memory = loadProjectMemory();
|
|
89
131
|
const learnings = memory.learnings || [];
|
|
90
132
|
const successfulFixes = learnings.filter((l) => l.success);
|
|
91
133
|
const selectorFixes = learnings.filter((l) => l.type === "selector_fix");
|
|
92
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
|
+
}
|
|
93
139
|
const totalTests = learnings.filter((l) => l.type === "test_generated").length;
|
|
94
140
|
const firstAttemptSuccess = learnings.filter((l) => l.type === "test_generated" && l.passedFirstTime).length;
|
|
95
141
|
return {
|
|
@@ -97,6 +143,7 @@ function getMemoryStats() {
|
|
|
97
143
|
successfulFixes: successfulFixes.length,
|
|
98
144
|
selectorFixes: selectorFixes.length,
|
|
99
145
|
timingFixes: timingFixes.length,
|
|
146
|
+
byLearningType,
|
|
100
147
|
testsGenerated: totalTests,
|
|
101
148
|
firstAttemptSuccessRate: totalTests > 0 ? Math.round(firstAttemptSuccess / totalTests * 100) : 0
|
|
102
149
|
};
|
|
@@ -140,6 +187,95 @@ var FLAKY_PATTERNS = [
|
|
|
140
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." },
|
|
141
188
|
{ name: "shared_state", regex: /state|cleanup|beforeEach|afterEach|isolation/i, suggestion: "Garanta beforeEach/afterEach para resetar estado. Evite vari\xE1veis globais compartilhadas." }
|
|
142
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 inferFailurePattern(runOutput, framework = "") {
|
|
250
|
+
const output = (runOutput || "").toLowerCase();
|
|
251
|
+
for (const p of FAILURE_ANALYSIS_PATTERNS) {
|
|
252
|
+
if (p.mobileOnly && !/appium|detox/i.test(framework)) continue;
|
|
253
|
+
if (p.regex.test(output)) return p;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
var MOBILE_MAPPING_LESSON = `Em testes mobile (Appium/Detox), SEMPRE inclua o mapeamento de elementos de forma VIS\xCDVEL e estruturada no c\xF3digo:
|
|
258
|
+
- Use constantes ou Page Object no TOPO do spec: const ELEMENTS = { loginBtn: '~btn_login', ... };
|
|
259
|
+
- No teste: $(ELEMENTS.loginBtn).click();
|
|
260
|
+
- Nunca deixe seletores "invis\xEDveis" (hardcoded inline repetidos). Isso dificulta manuten\xE7\xE3o e causa falhas.`;
|
|
261
|
+
var UNIVERSAL_TEST_PRACTICES = `PR\xC1TICAS OBRIGAT\xD3RIAS em todo teste gerado:
|
|
262
|
+
1. Esperas inteligentes: ANTES de interagir, verifique que o elemento est\xE1 dispon\xEDvel (waitForDisplayed, waitForExist, waitForSelector)
|
|
263
|
+
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'))
|
|
264
|
+
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`;
|
|
265
|
+
function formatLearnedMessageForUser({ pattern, fixSummary, runOutput, framework }) {
|
|
266
|
+
const p = pattern || (runOutput ? inferFailurePattern(runOutput, framework) : null);
|
|
267
|
+
const oQueAconteceu = p?.oQueAconteceu || "O teste falhou por um problema de elemento ou timing.";
|
|
268
|
+
const oQueFiz = fixSummary || (p ? `Apliquei a corre\xE7\xE3o para esse tipo de falha: ${p.name}.` : "Ajustei o c\xF3digo.");
|
|
269
|
+
return `**Entendi o erro e apliquei a corre\xE7\xE3o**
|
|
270
|
+
|
|
271
|
+
**O que aconteceu:** ${oQueAconteceu}
|
|
272
|
+
|
|
273
|
+
**O que fiz:** ${oQueFiz}
|
|
274
|
+
|
|
275
|
+
**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.
|
|
276
|
+
|
|
277
|
+
Use \`mcp-lab-agent stats\` ou \`get_learning_report\` para ver a evolu\xE7\xE3o dos aprendizados.`;
|
|
278
|
+
}
|
|
143
279
|
function detectFlakyPatterns(runOutput) {
|
|
144
280
|
const detected = [];
|
|
145
281
|
for (const p of FLAKY_PATTERNS) {
|
|
@@ -213,6 +349,9 @@ function detectProjectStructure() {
|
|
|
213
349
|
structure.hasTests = true;
|
|
214
350
|
structure.hasMobile = true;
|
|
215
351
|
}
|
|
352
|
+
if (deps["react-native"]) {
|
|
353
|
+
structure.hasMobile = true;
|
|
354
|
+
}
|
|
216
355
|
if (deps.supertest) {
|
|
217
356
|
structure.testFrameworks.push("supertest");
|
|
218
357
|
structure.hasTests = true;
|
|
@@ -369,6 +508,22 @@ function detectProjectStructure() {
|
|
|
369
508
|
}
|
|
370
509
|
}
|
|
371
510
|
}
|
|
511
|
+
const hints = [];
|
|
512
|
+
if (structure.hasMobile) hints.push("mobile");
|
|
513
|
+
if (structure.testFrameworks.includes("appium")) hints.push("appium");
|
|
514
|
+
if (structure.testFrameworks.includes("detox")) hints.push("detox");
|
|
515
|
+
const pkg = structure.packageJson || {};
|
|
516
|
+
const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
517
|
+
if (allDeps["react-native"]) hints.push("react-native");
|
|
518
|
+
const webFrameworks = ["cypress", "playwright", "webdriverio", "selenium", "puppeteer", "testcafe"];
|
|
519
|
+
const hasWebFrameworks = structure.testFrameworks.some((f) => webFrameworks.includes(f));
|
|
520
|
+
if (hasWebFrameworks) hints.push("web");
|
|
521
|
+
if (structure.testDirs.includes("mobile")) hints.push("mobile-dir");
|
|
522
|
+
let environment = "web";
|
|
523
|
+
if (structure.hasMobile && !hasWebFrameworks) environment = "mobile";
|
|
524
|
+
else if (structure.hasMobile && hasWebFrameworks) environment = "both";
|
|
525
|
+
structure.environment = environment;
|
|
526
|
+
structure.environmentHints = [...new Set(hints)];
|
|
372
527
|
return structure;
|
|
373
528
|
}
|
|
374
529
|
var UNIVERSAL_TEST_PATTERNS = [
|
|
@@ -573,7 +728,7 @@ var QA_AGENTS = {
|
|
|
573
728
|
browser: { desc: "Browser mode: screenshots, network, console", tools: ["web_eval_browser"] },
|
|
574
729
|
reporting: { desc: "Relat\xF3rios e m\xE9tricas", tools: ["create_bug_report", "get_business_metrics"] },
|
|
575
730
|
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"] },
|
|
576
|
-
learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats"] },
|
|
731
|
+
learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats", "get_learning_report"] },
|
|
577
732
|
maintenance: { desc: "Linter, deps, an\xE1lise de c\xF3digo", tools: ["run_linter", "install_dependencies"] }
|
|
578
733
|
};
|
|
579
734
|
function getExtensionAndBaseDir(fw, structure) {
|
|
@@ -593,14 +748,20 @@ USO:
|
|
|
593
748
|
mcp-lab-agent --help # Mostra esta ajuda
|
|
594
749
|
|
|
595
750
|
COMANDOS CLI:
|
|
596
|
-
|
|
751
|
+
slack-bot Inicia o Slack Bot (QA via @mention)
|
|
752
|
+
learning-hub Inicia o Learning Hub (API + Dashboard em http://localhost:3847)
|
|
753
|
+
analyze An\xE1lise completa: executa, analisa estabilidade, prev\xEA riscos e recomenda a\xE7\xF5es
|
|
597
754
|
auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
|
|
598
|
-
stats
|
|
755
|
+
stats Estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
|
|
756
|
+
report [--full] Relat\xF3rio de evolu\xE7\xE3o e aprendizado (--full = completo com recomenda\xE7\xF5es)
|
|
599
757
|
detect [--json] Detecta frameworks e estrutura
|
|
600
758
|
route <tarefa> Sugere qual ferramenta usar
|
|
601
759
|
list Lista ferramentas MCP dispon\xEDveis
|
|
602
760
|
|
|
603
761
|
EXEMPLOS:
|
|
762
|
+
mcp-lab-agent slack-bot # Slack Bot
|
|
763
|
+
mcp-lab-agent learning-hub # Learning Hub (API + Dashboard)
|
|
764
|
+
npx mcp-lab-agent slack-bot # Usar sem instalar (sem clonar o projeto)
|
|
604
765
|
mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
|
|
605
766
|
mcp-lab-agent auto "login flow" --max-retries 5
|
|
606
767
|
mcp-lab-agent stats
|
|
@@ -677,6 +838,8 @@ AMBIENTES CORPORATIVOS (APIs bloqueadas):
|
|
|
677
838
|
}
|
|
678
839
|
if (cmd === "stats") {
|
|
679
840
|
const stats = getMemoryStats();
|
|
841
|
+
const byType = stats.byLearningType || {};
|
|
842
|
+
const byTypeLines = Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => ` ${t}: ${v}`).join("\n");
|
|
680
843
|
console.log(`
|
|
681
844
|
\u{1F4CA} Estat\xEDsticas de Aprendizado
|
|
682
845
|
|
|
@@ -686,8 +849,47 @@ Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
|
|
|
686
849
|
Corre\xE7\xF5es de timing: ${stats.timingFixes}
|
|
687
850
|
Testes gerados: ${stats.testsGenerated}
|
|
688
851
|
Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
|
|
852
|
+
${byTypeLines ? `
|
|
853
|
+
Por tipo:
|
|
854
|
+
${byTypeLines}` : ""}
|
|
689
855
|
|
|
690
856
|
${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." : ""}
|
|
857
|
+
`);
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
if (cmd === "report") {
|
|
861
|
+
const memory = loadProjectMemory();
|
|
862
|
+
const learnings = memory.learnings || [];
|
|
863
|
+
const stats = getMemoryStats();
|
|
864
|
+
const byType = stats.byLearningType || {};
|
|
865
|
+
const format = process.argv.includes("--full") ? "full" : "summary";
|
|
866
|
+
const recommendations = [];
|
|
867
|
+
if (byType.element_not_rendered > 0 || byType.element_not_visible > 0) {
|
|
868
|
+
recommendations.push("Use waits expl\xEDcitos (waitForSelector, waitForDisplayed) ANTES de interagir com elementos.");
|
|
869
|
+
}
|
|
870
|
+
if (byType.timing_fix > 0 || byType.element_stale > 0) {
|
|
871
|
+
recommendations.push("Aumente timeouts e use re-localiza\xE7\xE3o de elementos em listas din\xE2micas.");
|
|
872
|
+
}
|
|
873
|
+
if (byType.selector_fix > 0 || byType.mobile_mapping_invisible > 0) {
|
|
874
|
+
recommendations.push("Priorize data-testid, role e seletores est\xE1veis; em mobile, use mapeamento vis\xEDvel no topo do spec.");
|
|
875
|
+
}
|
|
876
|
+
if (stats.firstAttemptSuccessRate < 70 && stats.testsGenerated > 0) {
|
|
877
|
+
recommendations.push("Aplique waits inteligentes + assert final em cada teste gerado.");
|
|
878
|
+
}
|
|
879
|
+
const byTypeStr = Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => ` - ${t}: ${v}`).join("\n");
|
|
880
|
+
console.log(`
|
|
881
|
+
\u{1F4C8} Relat\xF3rio de Evolu\xE7\xE3o e Aprendizado
|
|
882
|
+
|
|
883
|
+
Resumo por tipo:
|
|
884
|
+
${byTypeStr || " Nenhum aprendizado por tipo ainda"}
|
|
885
|
+
|
|
886
|
+
M\xE9tricas gerais:
|
|
887
|
+
Total de aprendizados: ${stats.totalLearnings}
|
|
888
|
+
Taxa de sucesso (1\xAA tentativa): ${stats.firstAttemptSuccessRate}%
|
|
889
|
+
Testes gerados: ${stats.testsGenerated}
|
|
890
|
+
${format === "full" && recommendations.length > 0 ? `
|
|
891
|
+
Recomenda\xE7\xF5es para aprimorar o c\xF3digo:
|
|
892
|
+
${recommendations.map((r) => ` \u2022 ${r}`).join("\n")}` : ""}
|
|
691
893
|
`);
|
|
692
894
|
return true;
|
|
693
895
|
}
|
|
@@ -979,7 +1181,7 @@ server.registerTool(
|
|
|
979
1181
|
"detect_project",
|
|
980
1182
|
{
|
|
981
1183
|
title: "Detectar estrutura do projeto",
|
|
982
|
-
description: "Analisa o projeto e identifica frameworks de teste, pastas, backend, frontend.",
|
|
1184
|
+
description: "Analisa o projeto e identifica frameworks de teste, pastas, backend, frontend, ambiente (web/mobile) e hints para gera\xE7\xE3o de testes.",
|
|
983
1185
|
inputSchema: z.object({}),
|
|
984
1186
|
outputSchema: z.object({
|
|
985
1187
|
ok: z.boolean(),
|
|
@@ -990,17 +1192,22 @@ server.registerTool(
|
|
|
990
1192
|
hasBackend: z.boolean(),
|
|
991
1193
|
backendDir: z.string().nullable(),
|
|
992
1194
|
hasFrontend: z.boolean(),
|
|
993
|
-
frontendDir: z.string().nullable()
|
|
1195
|
+
frontendDir: z.string().nullable(),
|
|
1196
|
+
hasMobile: z.boolean().optional(),
|
|
1197
|
+
environment: z.string().optional(),
|
|
1198
|
+
environmentHints: z.array(z.string()).optional()
|
|
994
1199
|
})
|
|
995
1200
|
})
|
|
996
1201
|
},
|
|
997
1202
|
async () => {
|
|
998
1203
|
const structure = detectProjectStructure();
|
|
1204
|
+
const envLine = structure.environment ? `Ambiente: ${structure.environment}${structure.environmentHints?.length ? ` (${structure.environmentHints.join(", ")})` : ""}` : "";
|
|
999
1205
|
const summary = [
|
|
1000
1206
|
`Frameworks de teste: ${structure.testFrameworks.join(", ") || "nenhum"}`,
|
|
1001
1207
|
`Pastas de teste: ${structure.testDirs.join(", ") || "nenhuma"}`,
|
|
1002
1208
|
`Backend: ${structure.backendDir || "n\xE3o detectado"}`,
|
|
1003
|
-
`Frontend: ${structure.frontendDir || "n\xE3o detectado"}
|
|
1209
|
+
`Frontend: ${structure.frontendDir || "n\xE3o detectado"}`,
|
|
1210
|
+
...envLine ? [envLine] : []
|
|
1004
1211
|
].join("\n");
|
|
1005
1212
|
return {
|
|
1006
1213
|
content: [{ type: "text", text: summary }],
|
|
@@ -1100,11 +1307,11 @@ var QA_AGENTS2 = {
|
|
|
1100
1307
|
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" },
|
|
1101
1308
|
detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
|
|
1102
1309
|
execution: { tools: ["run_tests", "watch_tests", "get_test_coverage"], desc: "Execu\xE7\xE3o de testes e cobertura" },
|
|
1103
|
-
generation: { tools: ["generate_tests", "write_test", "create_test_template"], desc: "Gera\xE7\xE3o de testes com LLM" },
|
|
1310
|
+
generation: { tools: ["generate_tests", "write_test", "create_test_template", "map_mobile_elements"], desc: "Gera\xE7\xE3o de testes com LLM" },
|
|
1104
1311
|
analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
|
|
1105
1312
|
browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
|
|
1106
1313
|
reporting: { tools: ["create_bug_report", "get_business_metrics"], desc: "Relat\xF3rios e m\xE9tricas" },
|
|
1107
|
-
learning: { tools: ["qa_learning_stats", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
|
|
1314
|
+
learning: { tools: ["qa_learning_stats", "get_learning_report", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
|
|
1108
1315
|
maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
|
|
1109
1316
|
};
|
|
1110
1317
|
server.registerTool(
|
|
@@ -1136,8 +1343,17 @@ server.registerTool(
|
|
|
1136
1343
|
if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
|
|
1137
1344
|
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 } };
|
|
1138
1345
|
}
|
|
1346
|
+
if (/mapear|elementos mobile|deep link|deeplink|app package|bundle.?id|appium inspector/i.test(t)) {
|
|
1347
|
+
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 } };
|
|
1348
|
+
}
|
|
1349
|
+
if (/mapear|elementos mobile|deep link|deeplink|app package|bundle.?id/i.test(t)) {
|
|
1350
|
+
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" } };
|
|
1351
|
+
}
|
|
1352
|
+
if (/mobile|deeplink|deep link|elementos|mapear.*app|appium|detox/i.test(t) && !/rodar|run|executar/i.test(t)) {
|
|
1353
|
+
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 } };
|
|
1354
|
+
}
|
|
1139
1355
|
if (/gerar|criar|escrever|generate|write|template/i.test(t)) {
|
|
1140
|
-
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 } };
|
|
1356
|
+
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 } };
|
|
1141
1357
|
}
|
|
1142
1358
|
if (/analisar|por que|falhou|suggest|correção|selector|fix/i.test(t)) {
|
|
1143
1359
|
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 } };
|
|
@@ -1461,8 +1677,15 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
|
|
|
1461
1677
|
- Mantenha a MESMA l\xF3gica e fluxo de teste
|
|
1462
1678
|
- Traduza seletores, comandos e asser\xE7\xF5es para ${fw}
|
|
1463
1679
|
- Use Page Objects se o projeto j\xE1 usa
|
|
1464
|
-
- Retorne SOMENTE o c\xF3digo, sem markdown
|
|
1680
|
+
- Retorne SOMENTE o c\xF3digo, sem markdown
|
|
1681
|
+
|
|
1682
|
+
${UNIVERSAL_TEST_PRACTICES}
|
|
1683
|
+
${fw === "appium" || fw === "detox" ? `
|
|
1684
|
+
IMPORTANTE: ${MOBILE_MAPPING_LESSON}` : ""}` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
|
|
1465
1685
|
Framework: ${fw}
|
|
1686
|
+
|
|
1687
|
+
${UNIVERSAL_TEST_PRACTICES}
|
|
1688
|
+
|
|
1466
1689
|
Regras:
|
|
1467
1690
|
- Cypress: cy.request(), cy.visit(), cy.get()
|
|
1468
1691
|
- Playwright: test(), test.describe(), page.goto(), page.locator()
|
|
@@ -1470,7 +1693,9 @@ Regras:
|
|
|
1470
1693
|
- Jest/Vitest: describe(), test(), expect()
|
|
1471
1694
|
- Robot: Keywords, [Tags], Steps
|
|
1472
1695
|
- pytest: def test_*, assert, fixtures
|
|
1473
|
-
- C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown
|
|
1696
|
+
- C\xF3digo limpo. Retorne SOMENTE o c\xF3digo, sem markdown${fw === "appium" || fw === "detox" ? `
|
|
1697
|
+
|
|
1698
|
+
IMPORTANTE (Appium/Detox): ${MOBILE_MAPPING_LESSON}` : ""}`;
|
|
1474
1699
|
const userPrompt = `Contexto do projeto:
|
|
1475
1700
|
${contextWithMemory.slice(0, 5e3)}
|
|
1476
1701
|
|
|
@@ -2039,6 +2264,114 @@ ${data.codigoCorrigido}
|
|
|
2039
2264
|
}
|
|
2040
2265
|
}
|
|
2041
2266
|
);
|
|
2267
|
+
server.registerTool(
|
|
2268
|
+
"map_mobile_elements",
|
|
2269
|
+
{
|
|
2270
|
+
title: "Mapear elementos mobile (estrutura para testes)",
|
|
2271
|
+
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.",
|
|
2272
|
+
inputSchema: z.object({
|
|
2273
|
+
deepLink: z.string().optional().describe("Deep link do app (ex: meuapp://login). Indica ambiente mobile."),
|
|
2274
|
+
appPackage: z.string().optional().describe("Android: package do app (ex: com.example.app)."),
|
|
2275
|
+
appActivity: z.string().optional().describe("Android: activity principal (ex: .MainActivity)."),
|
|
2276
|
+
bundleId: z.string().optional().describe("iOS: bundle identifier do app."),
|
|
2277
|
+
elementsJsonPath: z.string().optional().describe("Caminho para arquivo JSON com elementos mapeados (id, text, accessibilityId, xpath).")
|
|
2278
|
+
}),
|
|
2279
|
+
outputSchema: z.object({
|
|
2280
|
+
ok: z.boolean(),
|
|
2281
|
+
environment: z.string().optional(),
|
|
2282
|
+
elements: z.array(z.object({
|
|
2283
|
+
id: z.string().optional(),
|
|
2284
|
+
text: z.string().optional(),
|
|
2285
|
+
accessibilityId: z.string().optional(),
|
|
2286
|
+
xpath: z.string().optional(),
|
|
2287
|
+
resourceId: z.string().optional(),
|
|
2288
|
+
className: z.string().optional()
|
|
2289
|
+
})).optional(),
|
|
2290
|
+
instructions: z.string().optional(),
|
|
2291
|
+
contextForGenerate: z.string().optional().describe("Texto formatado para passar em generate_tests como contexto."),
|
|
2292
|
+
error: z.string().optional()
|
|
2293
|
+
})
|
|
2294
|
+
},
|
|
2295
|
+
async ({ deepLink, appPackage, appActivity, bundleId, elementsJsonPath }) => {
|
|
2296
|
+
const hasMobileContext = deepLink || appPackage || bundleId;
|
|
2297
|
+
const elements = [];
|
|
2298
|
+
let instructions = "";
|
|
2299
|
+
let contextForGenerate = "";
|
|
2300
|
+
if (elementsJsonPath) {
|
|
2301
|
+
const fullPath = path5.join(PROJECT_ROOT5, elementsJsonPath.replace(/^\//, "").replace(/\\/g, "/"));
|
|
2302
|
+
if (fs5.existsSync(fullPath)) {
|
|
2303
|
+
try {
|
|
2304
|
+
const raw = fs5.readFileSync(fullPath, "utf8");
|
|
2305
|
+
const parsed = JSON.parse(raw);
|
|
2306
|
+
const arr = Array.isArray(parsed) ? parsed : parsed.elements || parsed.items || [];
|
|
2307
|
+
arr.forEach((el) => {
|
|
2308
|
+
elements.push({
|
|
2309
|
+
id: el.id || el.resourceId,
|
|
2310
|
+
text: el.text || el.label,
|
|
2311
|
+
accessibilityId: el.accessibilityId || el["content-desc"] || el.contentDesc,
|
|
2312
|
+
xpath: el.xpath,
|
|
2313
|
+
resourceId: el.resourceId || el.id,
|
|
2314
|
+
className: el.className || el.class
|
|
2315
|
+
});
|
|
2316
|
+
});
|
|
2317
|
+
contextForGenerate = `
|
|
2318
|
+
Elementos mapeados da tela (use para seletores est\xE1veis em Appium/WDIO):
|
|
2319
|
+
${JSON.stringify(elements, null, 2)}
|
|
2320
|
+
`;
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
return {
|
|
2323
|
+
content: [{ type: "text", text: `Erro ao ler ${elementsJsonPath}: ${err.message}` }],
|
|
2324
|
+
structuredContent: { ok: false, error: err.message }
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
} else {
|
|
2328
|
+
return {
|
|
2329
|
+
content: [{ type: "text", text: `Arquivo n\xE3o encontrado: ${elementsJsonPath}` }],
|
|
2330
|
+
structuredContent: { ok: false, error: "File not found" }
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
if (hasMobileContext || elementsJsonPath) {
|
|
2335
|
+
instructions = [
|
|
2336
|
+
"## Como mapear elementos do app mobile",
|
|
2337
|
+
"",
|
|
2338
|
+
"**Android (Appium):**",
|
|
2339
|
+
"- Use Appium Inspector (appium.io) com appPackage/appActivity",
|
|
2340
|
+
"- Ou: `adb shell uiautomator dump` \u2192 analise o XML exportado",
|
|
2341
|
+
"- Priorize: accessibility-id > resource-id > xpath relativo",
|
|
2342
|
+
"",
|
|
2343
|
+
"**iOS (Appium):**",
|
|
2344
|
+
"- Appium Inspector com bundleId",
|
|
2345
|
+
"- Xcode Accessibility Inspector",
|
|
2346
|
+
"- Priorize: accessibility-id > name",
|
|
2347
|
+
"",
|
|
2348
|
+
"**Formato esperado (elements.json):**",
|
|
2349
|
+
"```json",
|
|
2350
|
+
'[{"accessibilityId": "login_btn", "text": "Entrar", "resourceId": "com.app:id/btn"}]',
|
|
2351
|
+
"```",
|
|
2352
|
+
"",
|
|
2353
|
+
"Salve em um arquivo e passe em `elementsJsonPath` na pr\xF3xima chamada."
|
|
2354
|
+
].join("\n");
|
|
2355
|
+
}
|
|
2356
|
+
const env = deepLink ? "mobile" : appPackage || bundleId ? "mobile" : elements.length ? "mobile" : "unknown";
|
|
2357
|
+
const text = [
|
|
2358
|
+
contextForGenerate && `## Contexto para generate_tests
|
|
2359
|
+
${contextForGenerate}`,
|
|
2360
|
+
instructions && `## Instru\xE7\xF5es
|
|
2361
|
+
${instructions}`
|
|
2362
|
+
].filter(Boolean).join("\n\n");
|
|
2363
|
+
return {
|
|
2364
|
+
content: [{ type: "text", text: text || (hasMobileContext ? `Ambiente: ${env}. ${instructions}` : "Informe deepLink, appPackage ou elementsJsonPath.") }],
|
|
2365
|
+
structuredContent: {
|
|
2366
|
+
ok: true,
|
|
2367
|
+
environment: env,
|
|
2368
|
+
elements: elements.length ? elements : void 0,
|
|
2369
|
+
instructions: instructions || void 0,
|
|
2370
|
+
contextForGenerate: contextForGenerate || void 0
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
);
|
|
2042
2375
|
server.registerTool(
|
|
2043
2376
|
"analyze_file_methods",
|
|
2044
2377
|
{
|
|
@@ -3007,6 +3340,70 @@ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Us
|
|
|
3007
3340
|
};
|
|
3008
3341
|
}
|
|
3009
3342
|
);
|
|
3343
|
+
server.registerTool(
|
|
3344
|
+
"get_learning_report",
|
|
3345
|
+
{
|
|
3346
|
+
title: "Relat\xF3rio de evolu\xE7\xE3o e aprendizado",
|
|
3347
|
+
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.",
|
|
3348
|
+
inputSchema: z.object({
|
|
3349
|
+
format: z.enum(["summary", "full"]).optional().describe("summary = resumo executivo, full = relat\xF3rio completo com recomenda\xE7\xF5es. Default: summary")
|
|
3350
|
+
}),
|
|
3351
|
+
outputSchema: z.object({
|
|
3352
|
+
summary: z.string(),
|
|
3353
|
+
byType: z.record(z.number()),
|
|
3354
|
+
evolution: z.array(z.object({ date: z.string(), type: z.string(), framework: z.string() })).optional(),
|
|
3355
|
+
recommendations: z.array(z.string()).optional()
|
|
3356
|
+
})
|
|
3357
|
+
},
|
|
3358
|
+
async ({ format = "summary" }) => {
|
|
3359
|
+
const memory = loadProjectMemory();
|
|
3360
|
+
const learnings = memory.learnings || [];
|
|
3361
|
+
const stats = getMemoryStats();
|
|
3362
|
+
const byType = stats.byLearningType || {};
|
|
3363
|
+
const evolution = format === "full" && learnings.length > 0 ? learnings.slice(-30).map((l) => ({
|
|
3364
|
+
date: (l.timestamp || "").slice(0, 10),
|
|
3365
|
+
type: l.type || "unknown",
|
|
3366
|
+
framework: l.framework || "-"
|
|
3367
|
+
})) : [];
|
|
3368
|
+
const recommendations = [];
|
|
3369
|
+
if (byType.element_not_rendered > 0 || byType.element_not_visible > 0) {
|
|
3370
|
+
recommendations.push("Use waits expl\xEDcitos (waitForSelector, waitForDisplayed) ANTES de interagir com elementos.");
|
|
3371
|
+
}
|
|
3372
|
+
if (byType.timing_fix > 0 || byType.element_stale > 0) {
|
|
3373
|
+
recommendations.push("Aumente timeouts e use re-localiza\xE7\xE3o de elementos em listas din\xE2micas.");
|
|
3374
|
+
}
|
|
3375
|
+
if (byType.selector_fix > 0 || byType.mobile_mapping_invisible > 0) {
|
|
3376
|
+
recommendations.push("Priorize data-testid, role e seletores est\xE1veis; em mobile, use mapeamento vis\xEDvel no topo do spec.");
|
|
3377
|
+
}
|
|
3378
|
+
if (stats.firstAttemptSuccessRate < 70 && stats.testsGenerated > 0) {
|
|
3379
|
+
recommendations.push("Aplique UNIVERSAL_TEST_PRACTICES em cada teste gerado: waits inteligentes + assert final.");
|
|
3380
|
+
}
|
|
3381
|
+
if (recommendations.length === 0 && learnings.length > 0) {
|
|
3382
|
+
recommendations.push("Continue aplicando as pr\xE1ticas aprendidas em novos testes.");
|
|
3383
|
+
}
|
|
3384
|
+
const summary = `\u{1F4C8} **Relat\xF3rio de Evolu\xE7\xE3o e Aprendizado**
|
|
3385
|
+
|
|
3386
|
+
**Resumo por tipo:**
|
|
3387
|
+
${Object.entries(byType).filter(([, v]) => v > 0).map(([t, v]) => `- ${t}: ${v}`).join("\n") || "- Nenhum aprendizado por tipo ainda"}
|
|
3388
|
+
|
|
3389
|
+
**M\xE9tricas gerais:**
|
|
3390
|
+
- Total de aprendizados: ${stats.totalLearnings}
|
|
3391
|
+
- Taxa de sucesso (1\xAA tentativa): ${stats.firstAttemptSuccessRate}%
|
|
3392
|
+
- Testes gerados: ${stats.testsGenerated}
|
|
3393
|
+
|
|
3394
|
+
${format === "full" && recommendations.length > 0 ? `**Recomenda\xE7\xF5es para aprimorar o c\xF3digo:**
|
|
3395
|
+
${recommendations.map((r) => `\u2022 ${r}`).join("\n")}` : ""}`;
|
|
3396
|
+
return {
|
|
3397
|
+
content: [{ type: "text", text: summary }],
|
|
3398
|
+
structuredContent: {
|
|
3399
|
+
summary: summary.trim(),
|
|
3400
|
+
byType,
|
|
3401
|
+
evolution: format === "full" ? evolution : void 0,
|
|
3402
|
+
recommendations: format === "full" ? recommendations : void 0
|
|
3403
|
+
}
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
);
|
|
3010
3407
|
server.registerTool(
|
|
3011
3408
|
"qa_compare_with_industry",
|
|
3012
3409
|
{
|
|
@@ -3299,14 +3696,16 @@ server.registerTool(
|
|
|
3299
3696
|
let testFilePath = null;
|
|
3300
3697
|
let testContent = null;
|
|
3301
3698
|
let attempt = 0;
|
|
3699
|
+
let appliedLearningFix = false;
|
|
3302
3700
|
learnings.push({ attempt: 0, action: "detect_project", result: `${structure.testFrameworks.length} framework(s)` });
|
|
3303
3701
|
for (attempt = 1; attempt <= maxRetries; attempt++) {
|
|
3304
3702
|
learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
|
|
3305
3703
|
const { provider, apiKey, baseUrl, model } = llm;
|
|
3306
|
-
const memoryHints = memory.learnings?.filter((l) => l.
|
|
3704
|
+
const memoryHints = memory.learnings?.filter((l) => l.fix).slice(-10).map((l) => l.fix).join("\n") || "";
|
|
3307
3705
|
const systemPrompt = `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
|
|
3308
|
-
${
|
|
3309
|
-
|
|
3706
|
+
${UNIVERSAL_TEST_PRACTICES}
|
|
3707
|
+
|
|
3708
|
+
${memoryHints ? `Aprendizados anteriores (use como refer\xEAncia):
|
|
3310
3709
|
${memoryHints.slice(0, 1e3)}` : ""}
|
|
3311
3710
|
Retorne SOMENTE o c\xF3digo, sem markdown.`;
|
|
3312
3711
|
const userPrompt = `Contexto:
|
|
@@ -3374,12 +3773,15 @@ Framework: ${fw}`;
|
|
|
3374
3773
|
saveProjectMemory({
|
|
3375
3774
|
learnings: [{ type: "test_generated", request, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
3376
3775
|
});
|
|
3776
|
+
const learnedAppendix2 = appliedLearningFix ? `
|
|
3777
|
+
|
|
3778
|
+
${formatLearnedMessageForUser({ runOutput: runResult?.output, fixSummary: "Ajustei o c\xF3digo aplicando waits e valida\xE7\xE3o correta.", framework: fw })}` : "";
|
|
3377
3779
|
return {
|
|
3378
3780
|
content: [{ type: "text", text: `\u2705 Teste passou na tentativa ${attempt}!
|
|
3379
3781
|
|
|
3380
3782
|
Arquivo: ${testFilePath}
|
|
3381
3783
|
|
|
3382
|
-
Aprendizados salvos
|
|
3784
|
+
Aprendizados salvos.${learnedAppendix2}` }],
|
|
3383
3785
|
structuredContent: { ok: true, testFilePath, attempts: attempt, finalStatus: "passed", learnings }
|
|
3384
3786
|
};
|
|
3385
3787
|
}
|
|
@@ -3389,11 +3791,14 @@ Aprendizados salvos.` }],
|
|
|
3389
3791
|
saveProjectMemory({
|
|
3390
3792
|
learnings: [{ type: "test_generated", request, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
3391
3793
|
});
|
|
3794
|
+
const learnedAppendix2 = appliedLearningFix ? `
|
|
3795
|
+
|
|
3796
|
+
${formatLearnedMessageForUser({ runOutput: runResult.output, framework: fw, fixSummary: "Tentei corrigir. Nas pr\xF3ximas execu\xE7\xF5es usarei esse aprendizado desde o in\xEDcio." })}` : "";
|
|
3392
3797
|
return {
|
|
3393
3798
|
content: [{ type: "text", text: `\u274C Teste falhou ap\xF3s ${attempt} tentativa(s).
|
|
3394
3799
|
|
|
3395
3800
|
\xDAltimo erro:
|
|
3396
|
-
${runResult.output.slice(0, 500)}` }],
|
|
3801
|
+
${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
|
|
3397
3802
|
structuredContent: { ok: false, testFilePath, attempts: attempt, finalStatus: "max_retries", learnings }
|
|
3398
3803
|
};
|
|
3399
3804
|
}
|
|
@@ -3411,16 +3816,21 @@ ${runResult.output.slice(0, 500)}` }],
|
|
|
3411
3816
|
fs5.writeFileSync(testFilePath, testContent, "utf8");
|
|
3412
3817
|
learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
|
|
3413
3818
|
if (flakyAnalysis.isLikelyFlaky) {
|
|
3819
|
+
const inferredPattern = inferFailurePattern(runResult.output, fw);
|
|
3820
|
+
const learningType = inferredPattern?.learningType || (flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix");
|
|
3821
|
+
const learningFix = inferredPattern?.lesson || fixedCode.slice(0, 500);
|
|
3414
3822
|
saveProjectMemory({
|
|
3415
3823
|
learnings: [{
|
|
3416
|
-
type:
|
|
3824
|
+
type: learningType,
|
|
3417
3825
|
request,
|
|
3418
3826
|
framework: fw,
|
|
3419
|
-
fix:
|
|
3827
|
+
fix: learningFix,
|
|
3828
|
+
pattern: inferredPattern?.name,
|
|
3420
3829
|
success: false,
|
|
3421
3830
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3422
3831
|
}]
|
|
3423
3832
|
});
|
|
3833
|
+
appliedLearningFix = true;
|
|
3424
3834
|
}
|
|
3425
3835
|
} catch (err) {
|
|
3426
3836
|
learnings.push({ attempt, action: "error", result: err.message });
|
|
@@ -3430,8 +3840,11 @@ ${runResult.output.slice(0, 500)}` }],
|
|
|
3430
3840
|
};
|
|
3431
3841
|
}
|
|
3432
3842
|
}
|
|
3843
|
+
const learnedAppendix = appliedLearningFix ? `
|
|
3844
|
+
|
|
3845
|
+
${formatLearnedMessageForUser({ fixSummary: "Tentei corrigir. Nas pr\xF3ximas execu\xE7\xF5es usarei esse aprendizado desde o in\xEDcio." })}` : "";
|
|
3433
3846
|
return {
|
|
3434
|
-
content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s)
|
|
3847
|
+
content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).${learnedAppendix}` }],
|
|
3435
3848
|
structuredContent: { ok: false, testFilePath, attempts: maxRetries, finalStatus: "max_retries", learnings }
|
|
3436
3849
|
};
|
|
3437
3850
|
}
|
|
@@ -3483,6 +3896,21 @@ test.describe('${type.toUpperCase()} Test', () => {
|
|
|
3483
3896
|
}
|
|
3484
3897
|
);
|
|
3485
3898
|
async function main() {
|
|
3899
|
+
const cmd = process.argv[2];
|
|
3900
|
+
if (cmd === "learning-hub") {
|
|
3901
|
+
const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
3902
|
+
const hubPath = path5.join(__dirname2, "..", "learning-hub", "src", "server.js");
|
|
3903
|
+
const hubUrl2 = pathToFileURL(hubPath).href;
|
|
3904
|
+
await import(hubUrl2);
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
if (cmd === "slack-bot") {
|
|
3908
|
+
const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
3909
|
+
const slackBotPath = path5.join(__dirname2, "..", "slack-bot", "src", "index.js");
|
|
3910
|
+
const slackBotUrl = pathToFileURL(slackBotPath).href;
|
|
3911
|
+
await import(slackBotUrl);
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3486
3914
|
const handled = await handleCLI();
|
|
3487
3915
|
if (handled) {
|
|
3488
3916
|
process.exit(0);
|