mcp-lab-agent 2.1.4 → 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 +190 -224
- package/dist/index.js +436 -20
- 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 +71 -16
- package/slack-bot/TROUBLESHOOTING.md +73 -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
|
@@ -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,95 @@ 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 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
|
+
}
|
|
144
279
|
function detectFlakyPatterns(runOutput) {
|
|
145
280
|
const detected = [];
|
|
146
281
|
for (const p of FLAKY_PATTERNS) {
|
|
@@ -214,6 +349,9 @@ function detectProjectStructure() {
|
|
|
214
349
|
structure.hasTests = true;
|
|
215
350
|
structure.hasMobile = true;
|
|
216
351
|
}
|
|
352
|
+
if (deps["react-native"]) {
|
|
353
|
+
structure.hasMobile = true;
|
|
354
|
+
}
|
|
217
355
|
if (deps.supertest) {
|
|
218
356
|
structure.testFrameworks.push("supertest");
|
|
219
357
|
structure.hasTests = true;
|
|
@@ -370,6 +508,22 @@ function detectProjectStructure() {
|
|
|
370
508
|
}
|
|
371
509
|
}
|
|
372
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)];
|
|
373
527
|
return structure;
|
|
374
528
|
}
|
|
375
529
|
var UNIVERSAL_TEST_PATTERNS = [
|
|
@@ -574,7 +728,7 @@ var QA_AGENTS = {
|
|
|
574
728
|
browser: { desc: "Browser mode: screenshots, network, console", tools: ["web_eval_browser"] },
|
|
575
729
|
reporting: { desc: "Relat\xF3rios e m\xE9tricas", tools: ["create_bug_report", "get_business_metrics"] },
|
|
576
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"] },
|
|
577
|
-
learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats"] },
|
|
731
|
+
learning: { desc: "Sistema de aprendizado", tools: ["qa_learning_stats", "get_learning_report"] },
|
|
578
732
|
maintenance: { desc: "Linter, deps, an\xE1lise de c\xF3digo", tools: ["run_linter", "install_dependencies"] }
|
|
579
733
|
};
|
|
580
734
|
function getExtensionAndBaseDir(fw, structure) {
|
|
@@ -594,16 +748,19 @@ USO:
|
|
|
594
748
|
mcp-lab-agent --help # Mostra esta ajuda
|
|
595
749
|
|
|
596
750
|
COMANDOS CLI:
|
|
597
|
-
slack-bot Inicia o Slack Bot (QA via @mention)
|
|
751
|
+
slack-bot Inicia o Slack Bot (QA via @mention)
|
|
752
|
+
learning-hub Inicia o Learning Hub (API + Dashboard em http://localhost:3847)
|
|
598
753
|
analyze An\xE1lise completa: executa, analisa estabilidade, prev\xEA riscos e recomenda a\xE7\xF5es
|
|
599
754
|
auto <descri\xE7\xE3o> [--max-retries N] Modo aut\xF4nomo: gera teste, roda, corrige e aprende (default: 3 tentativas)
|
|
600
|
-
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)
|
|
601
757
|
detect [--json] Detecta frameworks e estrutura
|
|
602
758
|
route <tarefa> Sugere qual ferramenta usar
|
|
603
759
|
list Lista ferramentas MCP dispon\xEDveis
|
|
604
760
|
|
|
605
761
|
EXEMPLOS:
|
|
606
|
-
mcp-lab-agent slack-bot # Slack Bot
|
|
762
|
+
mcp-lab-agent slack-bot # Slack Bot
|
|
763
|
+
mcp-lab-agent learning-hub # Learning Hub (API + Dashboard)
|
|
607
764
|
npx mcp-lab-agent slack-bot # Usar sem instalar (sem clonar o projeto)
|
|
608
765
|
mcp-lab-agent analyze # An\xE1lise completa + recomenda\xE7\xF5es
|
|
609
766
|
mcp-lab-agent auto "login flow" --max-retries 5
|
|
@@ -681,6 +838,8 @@ AMBIENTES CORPORATIVOS (APIs bloqueadas):
|
|
|
681
838
|
}
|
|
682
839
|
if (cmd === "stats") {
|
|
683
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");
|
|
684
843
|
console.log(`
|
|
685
844
|
\u{1F4CA} Estat\xEDsticas de Aprendizado
|
|
686
845
|
|
|
@@ -690,8 +849,47 @@ Corre\xE7\xF5es de seletores: ${stats.selectorFixes}
|
|
|
690
849
|
Corre\xE7\xF5es de timing: ${stats.timingFixes}
|
|
691
850
|
Testes gerados: ${stats.testsGenerated}
|
|
692
851
|
Taxa de sucesso na 1\xAA tentativa: ${stats.firstAttemptSuccessRate}%
|
|
852
|
+
${byTypeLines ? `
|
|
853
|
+
Por tipo:
|
|
854
|
+
${byTypeLines}` : ""}
|
|
693
855
|
|
|
694
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")}` : ""}
|
|
695
893
|
`);
|
|
696
894
|
return true;
|
|
697
895
|
}
|
|
@@ -983,7 +1181,7 @@ server.registerTool(
|
|
|
983
1181
|
"detect_project",
|
|
984
1182
|
{
|
|
985
1183
|
title: "Detectar estrutura do projeto",
|
|
986
|
-
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.",
|
|
987
1185
|
inputSchema: z.object({}),
|
|
988
1186
|
outputSchema: z.object({
|
|
989
1187
|
ok: z.boolean(),
|
|
@@ -994,17 +1192,22 @@ server.registerTool(
|
|
|
994
1192
|
hasBackend: z.boolean(),
|
|
995
1193
|
backendDir: z.string().nullable(),
|
|
996
1194
|
hasFrontend: z.boolean(),
|
|
997
|
-
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()
|
|
998
1199
|
})
|
|
999
1200
|
})
|
|
1000
1201
|
},
|
|
1001
1202
|
async () => {
|
|
1002
1203
|
const structure = detectProjectStructure();
|
|
1204
|
+
const envLine = structure.environment ? `Ambiente: ${structure.environment}${structure.environmentHints?.length ? ` (${structure.environmentHints.join(", ")})` : ""}` : "";
|
|
1003
1205
|
const summary = [
|
|
1004
1206
|
`Frameworks de teste: ${structure.testFrameworks.join(", ") || "nenhum"}`,
|
|
1005
1207
|
`Pastas de teste: ${structure.testDirs.join(", ") || "nenhuma"}`,
|
|
1006
1208
|
`Backend: ${structure.backendDir || "n\xE3o detectado"}`,
|
|
1007
|
-
`Frontend: ${structure.frontendDir || "n\xE3o detectado"}
|
|
1209
|
+
`Frontend: ${structure.frontendDir || "n\xE3o detectado"}`,
|
|
1210
|
+
...envLine ? [envLine] : []
|
|
1008
1211
|
].join("\n");
|
|
1009
1212
|
return {
|
|
1010
1213
|
content: [{ type: "text", text: summary }],
|
|
@@ -1104,11 +1307,11 @@ var QA_AGENTS2 = {
|
|
|
1104
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" },
|
|
1105
1308
|
detection: { tools: ["detect_project", "read_project", "list_test_files"], desc: "Detec\xE7\xE3o de estrutura, frameworks e arquivos" },
|
|
1106
1309
|
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" },
|
|
1310
|
+
generation: { tools: ["generate_tests", "write_test", "create_test_template", "map_mobile_elements"], desc: "Gera\xE7\xE3o de testes com LLM" },
|
|
1108
1311
|
analysis: { tools: ["analyze_failures", "por_que_falhou", "suggest_fix", "suggest_selector_fix"], desc: "An\xE1lise de falhas e sugest\xF5es" },
|
|
1109
1312
|
browser: { tools: ["web_eval_browser"], desc: "Avalia\xE7\xE3o em browser real (screenshots, network, console)" },
|
|
1110
1313
|
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" },
|
|
1314
|
+
learning: { tools: ["qa_learning_stats", "get_learning_report", "qa_time_travel"], desc: "Estat\xEDsticas de aprendizado e evolu\xE7\xE3o" },
|
|
1112
1315
|
maintenance: { tools: ["run_linter", "install_dependencies", "analyze_file_methods"], desc: "Manuten\xE7\xE3o e an\xE1lise de c\xF3digo" }
|
|
1113
1316
|
};
|
|
1114
1317
|
server.registerTool(
|
|
@@ -1140,8 +1343,17 @@ server.registerTool(
|
|
|
1140
1343
|
if (/rodar|executar|run|test|coverage|watch/i.test(t)) {
|
|
1141
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 } };
|
|
1142
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
|
+
}
|
|
1143
1355
|
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 } };
|
|
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 } };
|
|
1145
1357
|
}
|
|
1146
1358
|
if (/analisar|por que|falhou|suggest|correção|selector|fix/i.test(t)) {
|
|
1147
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 } };
|
|
@@ -1465,8 +1677,15 @@ O c\xF3digo de refer\xEAncia pode estar em QUALQUER framework (Cypress, Robot, P
|
|
|
1465
1677
|
- Mantenha a MESMA l\xF3gica e fluxo de teste
|
|
1466
1678
|
- Traduza seletores, comandos e asser\xE7\xF5es para ${fw}
|
|
1467
1679
|
- Use Page Objects se o projeto j\xE1 usa
|
|
1468
|
-
- 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.
|
|
1469
1685
|
Framework: ${fw}
|
|
1686
|
+
|
|
1687
|
+
${UNIVERSAL_TEST_PRACTICES}
|
|
1688
|
+
|
|
1470
1689
|
Regras:
|
|
1471
1690
|
- Cypress: cy.request(), cy.visit(), cy.get()
|
|
1472
1691
|
- Playwright: test(), test.describe(), page.goto(), page.locator()
|
|
@@ -1474,7 +1693,9 @@ Regras:
|
|
|
1474
1693
|
- Jest/Vitest: describe(), test(), expect()
|
|
1475
1694
|
- Robot: Keywords, [Tags], Steps
|
|
1476
1695
|
- pytest: def test_*, assert, fixtures
|
|
1477
|
-
- 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}` : ""}`;
|
|
1478
1699
|
const userPrompt = `Contexto do projeto:
|
|
1479
1700
|
${contextWithMemory.slice(0, 5e3)}
|
|
1480
1701
|
|
|
@@ -2043,6 +2264,114 @@ ${data.codigoCorrigido}
|
|
|
2043
2264
|
}
|
|
2044
2265
|
}
|
|
2045
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
|
+
);
|
|
2046
2375
|
server.registerTool(
|
|
2047
2376
|
"analyze_file_methods",
|
|
2048
2377
|
{
|
|
@@ -3011,6 +3340,70 @@ ${stats.totalLearnings === 0 ? "\u26A0\uFE0F Ainda n\xE3o h\xE1 aprendizados. Us
|
|
|
3011
3340
|
};
|
|
3012
3341
|
}
|
|
3013
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
|
+
);
|
|
3014
3407
|
server.registerTool(
|
|
3015
3408
|
"qa_compare_with_industry",
|
|
3016
3409
|
{
|
|
@@ -3303,14 +3696,16 @@ server.registerTool(
|
|
|
3303
3696
|
let testFilePath = null;
|
|
3304
3697
|
let testContent = null;
|
|
3305
3698
|
let attempt = 0;
|
|
3699
|
+
let appliedLearningFix = false;
|
|
3306
3700
|
learnings.push({ attempt: 0, action: "detect_project", result: `${structure.testFrameworks.length} framework(s)` });
|
|
3307
3701
|
for (attempt = 1; attempt <= maxRetries; attempt++) {
|
|
3308
3702
|
learnings.push({ attempt, action: "generate_tests", result: "gerando..." });
|
|
3309
3703
|
const { provider, apiKey, baseUrl, model } = llm;
|
|
3310
|
-
const memoryHints = memory.learnings?.filter((l) => l.
|
|
3704
|
+
const memoryHints = memory.learnings?.filter((l) => l.fix).slice(-10).map((l) => l.fix).join("\n") || "";
|
|
3311
3705
|
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
|
-
|
|
3706
|
+
${UNIVERSAL_TEST_PRACTICES}
|
|
3707
|
+
|
|
3708
|
+
${memoryHints ? `Aprendizados anteriores (use como refer\xEAncia):
|
|
3314
3709
|
${memoryHints.slice(0, 1e3)}` : ""}
|
|
3315
3710
|
Retorne SOMENTE o c\xF3digo, sem markdown.`;
|
|
3316
3711
|
const userPrompt = `Contexto:
|
|
@@ -3378,12 +3773,15 @@ Framework: ${fw}`;
|
|
|
3378
3773
|
saveProjectMemory({
|
|
3379
3774
|
learnings: [{ type: "test_generated", request, framework: fw, success: true, passedFirstTime: attempt === 1, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
3380
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 })}` : "";
|
|
3381
3779
|
return {
|
|
3382
3780
|
content: [{ type: "text", text: `\u2705 Teste passou na tentativa ${attempt}!
|
|
3383
3781
|
|
|
3384
3782
|
Arquivo: ${testFilePath}
|
|
3385
3783
|
|
|
3386
|
-
Aprendizados salvos
|
|
3784
|
+
Aprendizados salvos.${learnedAppendix2}` }],
|
|
3387
3785
|
structuredContent: { ok: true, testFilePath, attempts: attempt, finalStatus: "passed", learnings }
|
|
3388
3786
|
};
|
|
3389
3787
|
}
|
|
@@ -3393,11 +3791,14 @@ Aprendizados salvos.` }],
|
|
|
3393
3791
|
saveProjectMemory({
|
|
3394
3792
|
learnings: [{ type: "test_generated", request, framework: fw, success: false, attempts: attempt, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
3395
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." })}` : "";
|
|
3396
3797
|
return {
|
|
3397
3798
|
content: [{ type: "text", text: `\u274C Teste falhou ap\xF3s ${attempt} tentativa(s).
|
|
3398
3799
|
|
|
3399
3800
|
\xDAltimo erro:
|
|
3400
|
-
${runResult.output.slice(0, 500)}` }],
|
|
3801
|
+
${runResult.output.slice(0, 500)}${learnedAppendix2}` }],
|
|
3401
3802
|
structuredContent: { ok: false, testFilePath, attempts: attempt, finalStatus: "max_retries", learnings }
|
|
3402
3803
|
};
|
|
3403
3804
|
}
|
|
@@ -3415,16 +3816,21 @@ ${runResult.output.slice(0, 500)}` }],
|
|
|
3415
3816
|
fs5.writeFileSync(testFilePath, testContent, "utf8");
|
|
3416
3817
|
learnings.push({ attempt, action: "apply_fix", result: "corre\xE7\xE3o aplicada" });
|
|
3417
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);
|
|
3418
3822
|
saveProjectMemory({
|
|
3419
3823
|
learnings: [{
|
|
3420
|
-
type:
|
|
3824
|
+
type: learningType,
|
|
3421
3825
|
request,
|
|
3422
3826
|
framework: fw,
|
|
3423
|
-
fix:
|
|
3827
|
+
fix: learningFix,
|
|
3828
|
+
pattern: inferredPattern?.name,
|
|
3424
3829
|
success: false,
|
|
3425
3830
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3426
3831
|
}]
|
|
3427
3832
|
});
|
|
3833
|
+
appliedLearningFix = true;
|
|
3428
3834
|
}
|
|
3429
3835
|
} catch (err) {
|
|
3430
3836
|
learnings.push({ attempt, action: "error", result: err.message });
|
|
@@ -3434,8 +3840,11 @@ ${runResult.output.slice(0, 500)}` }],
|
|
|
3434
3840
|
};
|
|
3435
3841
|
}
|
|
3436
3842
|
}
|
|
3843
|
+
const learnedAppendix = appliedLearningFix ? `
|
|
3844
|
+
|
|
3845
|
+
${formatLearnedMessageForUser({ fixSummary: "Tentei corrigir. Nas pr\xF3ximas execu\xE7\xF5es usarei esse aprendizado desde o in\xEDcio." })}` : "";
|
|
3437
3846
|
return {
|
|
3438
|
-
content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s)
|
|
3847
|
+
content: [{ type: "text", text: `\u274C Falhou ap\xF3s ${maxRetries} tentativa(s).${learnedAppendix}` }],
|
|
3439
3848
|
structuredContent: { ok: false, testFilePath, attempts: maxRetries, finalStatus: "max_retries", learnings }
|
|
3440
3849
|
};
|
|
3441
3850
|
}
|
|
@@ -3488,6 +3897,13 @@ test.describe('${type.toUpperCase()} Test', () => {
|
|
|
3488
3897
|
);
|
|
3489
3898
|
async function main() {
|
|
3490
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
|
+
}
|
|
3491
3907
|
if (cmd === "slack-bot") {
|
|
3492
3908
|
const __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
3493
3909
|
const slackBotPath = path5.join(__dirname2, "..", "slack-bot", "src", "index.js");
|