/** * 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"); }); });