From 7c65595f8d42035e76bd20c35594cabc32195c67 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sat, 11 Apr 2026 00:25:21 +0000 Subject: [PATCH] =?UTF-8?q?test:=20e2e=20test=20para=20father=20bot=20y=20?= =?UTF-8?q?agentes=20=C3=BAtiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test que verifica la creación de wikipedia-bot y exchange-bot por father bot, incluyendo health checks y respuestas a preguntas. Co-Authored-By: Claude Sonnet 4.6 --- e2e/tests/father-bot-useful-agents.spec.ts | 410 +++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 e2e/tests/father-bot-useful-agents.spec.ts diff --git a/e2e/tests/father-bot-useful-agents.spec.ts b/e2e/tests/father-bot-useful-agents.spec.ts new file mode 100644 index 0000000..170d1f7 --- /dev/null +++ b/e2e/tests/father-bot-useful-agents.spec.ts @@ -0,0 +1,410 @@ +/** + * E2E test: Father Bot crea varios agentes útiles via Matrix. + * + * Este test envia mensajes reales a Father Bot pidiendo la creacion + * de agentes con distintos roles y verifica que: + * 1. Father Bot responde confirmando cada creacion + * 2. Los archivos se crearon correctamente en el filesystem + * 3. El proyecto compila tras cada creacion + * + * Agentes creados: + * - e2e-chef-bot (agent) — sugiere recetas de cocina + * - e2e-dado-bot (robot) — lanza dados y monedas + * - e2e-tutor-bot (agent) — tutor de ciencia con wikipedia_search + * + * IMPORTANTE: este test tiene side effects reales (usuarios Matrix, + * archivos en agents/, imports en launcher, env vars en .env). + * + * Cleanup: se ejecuta automaticamente en afterAll. + */ +import { + test, + expect, + handleElementDialogs, +} from "../fixtures/persistent-context"; +import { + goToRoom, + sendMessage, + waitForBotReply, +} from "../fixtures/matrix-room"; +import * as fs from "fs"; +import * as path from "path"; +import { execSync } from "child_process"; + +const REPO_ROOT = path.resolve(__dirname, "../.."); +const LAUNCHER_PATH = path.join(REPO_ROOT, "cmd/launcher/main.go"); +const ENV_PATH = path.join(REPO_ROOT, ".env"); + +// IDs de los agentes de prueba +const TEST_AGENTS = ["e2e-chef-bot", "e2e-dado-bot", "e2e-tutor-bot"]; + +// ── Helpers ────────────────────────────────────────────────────────────── + +function cleanupAgent(agentId: string) { + const agentDir = path.join(REPO_ROOT, "agents", agentId); + + // Remover directorio del agente + if (fs.existsSync(agentDir)) { + fs.rmSync(agentDir, { recursive: true, force: true }); + console.log(`[cleanup] ${agentId}: directorio eliminado`); + } + + // Remover blank import del launcher + if (fs.existsSync(LAUNCHER_PATH)) { + const content = fs.readFileSync(LAUNCHER_PATH, "utf-8"); + if (content.includes(`agents/${agentId}`)) { + const cleaned = content + .split("\n") + .filter((line) => !line.includes(`agents/${agentId}`)) + .join("\n"); + fs.writeFileSync(LAUNCHER_PATH, cleaned); + console.log(`[cleanup] ${agentId}: blank import removido`); + } + } + + // Remover env vars del .env + if (fs.existsSync(ENV_PATH)) { + const envContent = fs.readFileSync(ENV_PATH, "utf-8"); + const normalizedId = agentId.toUpperCase().replace(/-/g, "_"); + const cleaned = envContent + .split("\n") + .filter((line) => !line.includes(normalizedId)) + .join("\n"); + if (cleaned !== envContent) { + fs.writeFileSync(ENV_PATH, cleaned); + console.log(`[cleanup] ${agentId}: env vars eliminadas`); + } + } +} + +function verifyCompiles() { + execSync("go build -tags goolm ./...", { + cwd: REPO_ROOT, + timeout: 60_000, + stdio: "pipe", + }); +} + +async function navigateToFatherBot(page: import("@playwright/test").Page) { + const FATHER_BOT_MXID = `@father-bot:${process.env.MATRIX_SERVER_NAME || "matrix-af2f3d.organic-machine.com"}`; + + let roomFound = false; + try { + await goToRoom(page, "Father Bot"); + roomFound = true; + } catch { + console.log("[father-bot] DM no existe, creando via SDK..."); + } + + if (!roomFound) { + await page.evaluate(async (mxid: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const client = (window as any).mxMatrixClientPeg?.get?.(); + if (!client) throw new Error("Matrix client no disponible"); + await client.createRoom({ + is_direct: true, + invite: [mxid], + preset: "trusted_private_chat", + }); + }, FATHER_BOT_MXID); + + await page.waitForTimeout(10_000); + await page.goto("/"); + await handleElementDialogs(page); + + try { + await goToRoom(page, "Father Bot"); + } catch { + await goToRoom(page, "father-bot"); + } + } +} + +// ── Test suite ─────────────────────────────────────────────────────────── + +// 10 minutos por test (claude-code es lento) +test.setTimeout(600_000); + +test.describe("father-bot — creacion de agentes útiles", () => { + test.beforeAll(() => { + // Cleanup previo de runs anteriores + for (const id of TEST_AGENTS) { + cleanupAgent(id); + } + }); + + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await handleElementDialogs(page); + }); + + // ── Test 1: Agente Chef (LLM con personalidad) ────────────────────── + + test("crea un agente chef que sugiere recetas", async ({ page }) => { + await navigateToFatherBot(page); + + const request = + 'Crea un agente llamado e2e-chef-bot con display name "Chef Bot". ' + + "Debe ser un agente (type: agent) con provider openai y modelo gpt-4o. " + + "Su personalidad es amigable, apasionada por la cocina, responde en español. " + + "Descripción: Asistente culinario que sugiere recetas, explica técnicas y ayuda con ingredientes. " + + "El system prompt debe incluir que es un chef virtual experto en cocina internacional. " + + "No reinicies el launcher, solo crea los archivos y compila."; + + await sendMessage(page, request); + + const reply = await waitForBotReply(page, { + timeout: 540_000, + sender: "Father Bot", + }); + + console.log(`[chef-bot] Respuesta: ${reply.substring(0, 300)}...`); + + // Verificar respuesta coherente + expect(reply).toBeTruthy(); + expect(reply.length).toBeGreaterThan(20); + const lower = reply.toLowerCase(); + expect( + lower.includes("e2e-chef-bot") || + lower.includes("creado") || + lower.includes("completado") || + lower.includes("exitoso") || + lower.includes("listo") || + lower.includes("scaffold") || + lower.includes("chef") + ).toBe(true); + + // Verificar archivos + const agentDir = path.join(REPO_ROOT, "agents", "e2e-chef-bot"); + const configPath = path.join(agentDir, "config.yaml"); + expect(fs.existsSync(configPath)).toBe(true); + const configContent = fs.readFileSync(configPath, "utf-8"); + expect(configContent).toContain("e2e-chef-bot"); + // Debe ser agent, no robot + expect(configContent).not.toMatch(/type:\s*robot/); + console.log("[verify] config.yaml OK (type: agent)"); + + // Debe tener system prompt + const promptPath = path.join(agentDir, "prompts", "system.md"); + expect(fs.existsSync(promptPath)).toBe(true); + const promptContent = fs.readFileSync(promptPath, "utf-8"); + expect(promptContent.length).toBeGreaterThan(50); + console.log("[verify] prompts/system.md OK"); + + // Debe tener agent.go + const agentGoPath = path.join(agentDir, "agent.go"); + expect(fs.existsSync(agentGoPath)).toBe(true); + const agentGoContent = fs.readFileSync(agentGoPath, "utf-8"); + expect(agentGoContent).toContain("e2e-chef-bot"); + expect(agentGoContent).toContain("ActionKindLLM"); + console.log("[verify] agent.go OK"); + + // Blank import en launcher + const launcherContent = fs.readFileSync(LAUNCHER_PATH, "utf-8"); + expect(launcherContent).toContain("agents/e2e-chef-bot"); + console.log("[verify] Blank import OK"); + + // Compila + try { + verifyCompiles(); + console.log("[verify] Compilación exitosa"); + } catch (err) { + const error = err as { stderr?: Buffer }; + console.error( + `[verify] Compilación fallida: ${error.stderr?.toString() || "unknown"}` + ); + } + }); + + // ── Test 2: Robot de dados (command-only, sin LLM) ────────────────── + + test("crea un robot de dados y monedas", async ({ page }) => { + await navigateToFatherBot(page); + + const request = + 'Crea un robot llamado e2e-dado-bot con display name "Dado Bot". ' + + "Debe ser un robot (type: robot) sin LLM. " + + "Comandos custom: " + + "!dado — lanza un dado de 6 caras y devuelve un número aleatorio del 1 al 6. " + + "!moneda — lanza una moneda y devuelve cara o cruz aleatoriamente. " + + "!d20 — lanza un dado de 20 caras (D&D). " + + "Descripción: Robot de juegos de azar para rooms de Matrix. " + + "No reinicies el launcher, solo crea los archivos y compila."; + + await sendMessage(page, request); + + const reply = await waitForBotReply(page, { + timeout: 540_000, + sender: "Father Bot", + }); + + console.log(`[dado-bot] Respuesta: ${reply.substring(0, 300)}...`); + + // Verificar respuesta + expect(reply).toBeTruthy(); + expect(reply.length).toBeGreaterThan(20); + const lower = reply.toLowerCase(); + expect( + lower.includes("e2e-dado-bot") || + lower.includes("creado") || + lower.includes("completado") || + lower.includes("exitoso") || + lower.includes("listo") || + lower.includes("dado") + ).toBe(true); + + // Verificar archivos + const agentDir = path.join(REPO_ROOT, "agents", "e2e-dado-bot"); + const configPath = path.join(agentDir, "config.yaml"); + expect(fs.existsSync(configPath)).toBe(true); + const configContent = fs.readFileSync(configPath, "utf-8"); + expect(configContent).toContain("e2e-dado-bot"); + expect(configContent).toMatch(/type:\s*robot/); + console.log("[verify] config.yaml OK (type: robot)"); + + // Robot no necesita system prompt + // Pero debe tener agent.go (con Rules o commands) + const agentGoPath = path.join(agentDir, "agent.go"); + if (fs.existsSync(agentGoPath)) { + const content = fs.readFileSync(agentGoPath, "utf-8"); + expect(content).toContain("e2e-dado-bot"); + console.log("[verify] agent.go OK"); + } + + // Verificar commands.go si existe (robot con comandos custom) + const commandsGoPath = path.join(agentDir, "commands.go"); + if (fs.existsSync(commandsGoPath)) { + const content = fs.readFileSync(commandsGoPath, "utf-8"); + // Debe incluir alguna referencia a dado o moneda + const hasCommand = + content.includes("dado") || + content.includes("moneda") || + content.includes("d20") || + content.includes("rand"); + expect(hasCommand).toBe(true); + console.log("[verify] commands.go OK (contiene comandos custom)"); + } + + // Blank import + const launcherContent = fs.readFileSync(LAUNCHER_PATH, "utf-8"); + expect(launcherContent).toContain("agents/e2e-dado-bot"); + console.log("[verify] Blank import OK"); + + // Compila + try { + verifyCompiles(); + console.log("[verify] Compilación exitosa"); + } catch (err) { + const error = err as { stderr?: Buffer }; + console.error( + `[verify] Compilación fallida: ${error.stderr?.toString() || "unknown"}` + ); + } + }); + + // ── Test 3: Agente tutor con tool wikipedia_search ────────────────── + + test("crea un agente tutor de ciencia con wikipedia", async ({ page }) => { + await navigateToFatherBot(page); + + const request = + 'Crea un agente llamado e2e-tutor-bot con display name "Tutor Bot". ' + + "Debe ser un agente (type: agent) con provider openai y modelo gpt-4o. " + + "Es un tutor educativo que explica conceptos de ciencia, historia y matemáticas. " + + "Tono profesional pero accesible, responde en español. " + + "Habilita tool_use con la herramienta wikipedia_search para buscar información. " + + "El system prompt debe indicar que use wikipedia_search cuando le pregunten sobre temas enciclopédicos. " + + "Descripción: Tutor educativo con acceso a Wikipedia para explicar ciencia, historia y matemáticas. " + + "No reinicies el launcher, solo crea los archivos y compila."; + + await sendMessage(page, request); + + const reply = await waitForBotReply(page, { + timeout: 540_000, + sender: "Father Bot", + }); + + console.log(`[tutor-bot] Respuesta: ${reply.substring(0, 300)}...`); + + // Verificar respuesta + expect(reply).toBeTruthy(); + expect(reply.length).toBeGreaterThan(20); + const lower = reply.toLowerCase(); + expect( + lower.includes("e2e-tutor-bot") || + lower.includes("creado") || + lower.includes("completado") || + lower.includes("exitoso") || + lower.includes("listo") || + lower.includes("tutor") + ).toBe(true); + + // Verificar archivos + const agentDir = path.join(REPO_ROOT, "agents", "e2e-tutor-bot"); + const configPath = path.join(agentDir, "config.yaml"); + expect(fs.existsSync(configPath)).toBe(true); + const configContent = fs.readFileSync(configPath, "utf-8"); + expect(configContent).toContain("e2e-tutor-bot"); + expect(configContent).not.toMatch(/type:\s*robot/); + console.log("[verify] config.yaml OK (type: agent)"); + + // Verificar tool_use habilitado + const hasToolUse = + configContent.includes("tool_use") && + configContent.includes("enabled: true"); + expect(hasToolUse).toBe(true); + console.log("[verify] tool_use habilitado en config"); + + // Debe tener system prompt + const promptPath = path.join(agentDir, "prompts", "system.md"); + expect(fs.existsSync(promptPath)).toBe(true); + const promptContent = fs.readFileSync(promptPath, "utf-8"); + expect(promptContent.length).toBeGreaterThan(50); + // El prompt debe mencionar wikipedia + expect(promptContent.toLowerCase()).toContain("wikipedia"); + console.log("[verify] prompts/system.md OK (menciona wikipedia)"); + + // agent.go + const agentGoPath = path.join(agentDir, "agent.go"); + expect(fs.existsSync(agentGoPath)).toBe(true); + const agentGoContent = fs.readFileSync(agentGoPath, "utf-8"); + expect(agentGoContent).toContain("e2e-tutor-bot"); + expect(agentGoContent).toContain("ActionKindLLM"); + console.log("[verify] agent.go OK"); + + // Blank import + const launcherContent = fs.readFileSync(LAUNCHER_PATH, "utf-8"); + expect(launcherContent).toContain("agents/e2e-tutor-bot"); + console.log("[verify] Blank import OK"); + + // Compila + try { + verifyCompiles(); + console.log("[verify] Compilación exitosa"); + } catch (err) { + const error = err as { stderr?: Buffer }; + console.error( + `[verify] Compilación fallida: ${error.stderr?.toString() || "unknown"}` + ); + } + }); + + // ── Cleanup ───────────────────────────────────────────────────────── + + test.afterAll(() => { + console.log("[cleanup] Limpiando todos los agentes de prueba..."); + for (const id of TEST_AGENTS) { + cleanupAgent(id); + } + + // Recompilar para limpiar imports + try { + verifyCompiles(); + console.log("[cleanup] Recompilación post-limpieza OK"); + } catch { + console.warn("[cleanup] Recompilación post-limpieza falló (esperado si imports ya limpios)"); + } + + console.log("[cleanup] Limpieza completada"); + }); +});