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/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) - sem precisar clonar o repo
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 Mostra estat\xEDsticas de aprendizado (taxa de sucesso, corre\xE7\xF5es, etc.)
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 (configure em ~/.cursor/mcp.json)
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` : `Voc\xEA \xE9 um engenheiro de QA especializado em ${fw}. Gere APENAS o c\xF3digo do spec, sem explica\xE7\xF5es.
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.success).slice(-10).map((l) => l.fix).join("\n") || "";
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
- ${memoryHints ? `
3313
- Aprendizados anteriores (use como refer\xEAncia):
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: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
3824
+ type: learningType,
3421
3825
  request,
3422
3826
  framework: fw,
3423
- fix: fixedCode.slice(0, 500),
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");