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/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
- analyze [NOVO] An\xE1lise completa: executa, analisa estabilidade, prev\xEA riscos e recomenda a\xE7\xF5es
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 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)
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` : `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.
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.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") || "";
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
- ${memoryHints ? `
3309
- Aprendizados anteriores (use como refer\xEAncia):
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: flakyAnalysis.patterns[0]?.pattern === "selector" ? "selector_fix" : "timing_fix",
3824
+ type: learningType,
3417
3825
  request,
3418
3826
  framework: fw,
3419
- fix: fixedCode.slice(0, 500),
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);