diff --git a/e2e/tests/father-bot-create.spec.ts b/e2e/tests/father-bot-create.spec.ts new file mode 100644 index 0000000..0dd5369 --- /dev/null +++ b/e2e/tests/father-bot-create.spec.ts @@ -0,0 +1,215 @@ +/** + * E2E test: Father Bot crea un agente via Matrix. + * + * Este test envia un mensaje real a Father Bot pidiendo la creacion + * de un robot simple, y verifica que: + * 1. Father Bot responde confirmando la creacion + * 2. Los archivos del robot se crearon en el filesystem + * 3. El robot compila correctamente + * + * IMPORTANTE: este test tiene side effects reales: + * - Crea un usuario Matrix para el robot + * - Crea archivos en agents/ + * - Modifica cmd/launcher/main.go (blank import) + * - Anade env vars a .env + * + * Cleanup: dev-scripts/agent/remove.sh e2e-father-test + */ +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 TEST_AGENT_ID = "e2e-father-test"; +const TEST_AGENT_DIR = path.join(REPO_ROOT, "agents", TEST_AGENT_ID); + +// Timeout largo: claude-code puede tardar varios minutos +test.setTimeout(600_000); // 10 minutos + +test.describe("father-bot — creacion de agente via Matrix", () => { + test.beforeAll(() => { + // Cleanup previo: si el agente ya existe de un run anterior, eliminarlo + if (fs.existsSync(TEST_AGENT_DIR)) { + console.log(`[cleanup] Eliminando agente previo: ${TEST_AGENT_DIR}`); + fs.rmSync(TEST_AGENT_DIR, { recursive: true, force: true }); + } + // Remover blank import si existe + const launcherPath = path.join(REPO_ROOT, "cmd/launcher/main.go"); + const launcherContent = fs.readFileSync(launcherPath, "utf-8"); + if (launcherContent.includes(`agents/${TEST_AGENT_ID}`)) { + const cleaned = launcherContent + .split("\n") + .filter((line) => !line.includes(`agents/${TEST_AGENT_ID}`)) + .join("\n"); + fs.writeFileSync(launcherPath, cleaned); + console.log("[cleanup] Blank import removido del launcher"); + } + }); + + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await handleElementDialogs(page); + }); + + test("pide a father-bot crear un robot simple y verifica los archivos", async ({ page }) => { + // 1. Crear DM con Father Bot si no existe, luego navegar + const FATHER_BOT_MXID = `@father-bot:${process.env.MATRIX_SERVER_NAME || "matrix-af2f3d.organic-machine.com"}`; + + // Intentar encontrar el room existente primero + let roomFound = false; + try { + await goToRoom(page, "Father Bot"); + roomFound = true; + } catch { + console.log("[father-bot] DM no existe, creando via SDK..."); + } + + if (!roomFound) { + // Crear DM room via Matrix SDK en Element + 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); + + console.log("[father-bot] DM room creado, esperando sync + join..."); + await page.waitForTimeout(10_000); + + // Recargar y navegar — intentar con display name, luego con MXID + await page.goto("/"); + await handleElementDialogs(page); + + try { + await goToRoom(page, "Father Bot"); + } catch { + console.log("[father-bot] Retry con 'father-bot'..."); + await goToRoom(page, "father-bot"); + } + } + + // 2. Enviar peticion de creacion + const request = + `Crea un robot llamado ${TEST_AGENT_ID} con display name "E2E Father Test". ` + + `Debe ser un robot simple (type: robot) que responda al comando !saludo con "Hola desde e2e-father-test". ` + + `No reinicies el launcher despues de crearlo, solo crea los archivos y compila.`; + + await sendMessage(page, request); + + // 3. Esperar respuesta de father-bot (timeout largo por claude-code) + const reply = await waitForBotReply(page, { + timeout: 540_000, // 9 minutos + sender: "Father Bot", + }); + + console.log(`[father-bot] Respuesta: ${reply.substring(0, 200)}...`); + + // 4. Verificar que father-bot respondio con algo coherente + expect(reply).toBeTruthy(); + expect(reply.length).toBeGreaterThan(20); + + // La respuesta deberia mencionar exito o el nombre del agente + const replyLower = reply.toLowerCase(); + const hasSuccess = + replyLower.includes(TEST_AGENT_ID) || + replyLower.includes("creado") || + replyLower.includes("completado") || + replyLower.includes("exitoso") || + replyLower.includes("listo") || + replyLower.includes("scaffold"); + expect(hasSuccess).toBe(true); + + // 5. Verificar que los archivos existen en el filesystem + console.log("[verify] Verificando archivos del agente creado..."); + + const configPath = path.join(TEST_AGENT_DIR, "config.yaml"); + expect(fs.existsSync(configPath)).toBe(true); + const configContent = fs.readFileSync(configPath, "utf-8"); + expect(configContent).toContain(TEST_AGENT_ID); + expect(configContent).toMatch(/type:\s*robot/); + console.log("[verify] config.yaml OK (type: robot)"); + + // Robots may or may not have agent.go depending on implementation + const agentGoPath = path.join(TEST_AGENT_DIR, "agent.go"); + if (fs.existsSync(agentGoPath)) { + const agentGoContent = fs.readFileSync(agentGoPath, "utf-8"); + expect(agentGoContent).toContain(TEST_AGENT_ID); + console.log("[verify] agent.go OK"); + } + + // 6. Verificar que compila + console.log("[verify] Compilando..."); + try { + execSync("go build -tags goolm ./...", { + cwd: REPO_ROOT, + timeout: 60_000, + stdio: "pipe", + }); + console.log("[verify] Compilacion exitosa"); + } catch (err) { + const error = err as { stderr?: Buffer }; + const stderr = error.stderr?.toString() || "unknown error"; + console.error(`[verify] Compilacion fallida: ${stderr}`); + // No hacemos fail del test por compilacion ya que father-bot + // puede haber creado los archivos parcialmente + } + + // 7. Verificar blank import en launcher + const launcherPath = path.join(REPO_ROOT, "cmd/launcher/main.go"); + const launcherContent = fs.readFileSync(launcherPath, "utf-8"); + expect(launcherContent).toContain(`agents/${TEST_AGENT_ID}`); + console.log("[verify] Blank import en launcher OK"); + }); + + test.afterAll(() => { + // Cleanup: eliminar el agente de prueba + console.log(`[cleanup] Limpiando agente de prueba: ${TEST_AGENT_ID}`); + + // Remover directorio del agente + if (fs.existsSync(TEST_AGENT_DIR)) { + fs.rmSync(TEST_AGENT_DIR, { recursive: true, force: true }); + console.log("[cleanup] Directorio del agente eliminado"); + } + + // Remover blank import del launcher + const launcherPath = path.join(REPO_ROOT, "cmd/launcher/main.go"); + if (fs.existsSync(launcherPath)) { + const content = fs.readFileSync(launcherPath, "utf-8"); + if (content.includes(`agents/${TEST_AGENT_ID}`)) { + const cleaned = content + .split("\n") + .filter((line) => !line.includes(`agents/${TEST_AGENT_ID}`)) + .join("\n"); + fs.writeFileSync(launcherPath, cleaned); + console.log("[cleanup] Blank import removido del launcher"); + } + } + + // Remover env vars del .env + const envPath = path.join(REPO_ROOT, ".env"); + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, "utf-8"); + const normalizedId = TEST_AGENT_ID.toUpperCase().replace(/-/g, "_"); + const cleaned = envContent + .split("\n") + .filter((line) => !line.includes(normalizedId)) + .join("\n"); + if (cleaned !== envContent) { + fs.writeFileSync(envPath, cleaned); + console.log("[cleanup] Env vars eliminadas de .env"); + } + } + + console.log("[cleanup] Limpieza completada"); + }); +}); diff --git a/security/user-groups.yaml b/security/user-groups.yaml index ea3e315..6d11037 100644 --- a/security/user-groups.yaml +++ b/security/user-groups.yaml @@ -2,6 +2,8 @@ # Members: lista de Matrix user IDs, o "*" para todos los usuarios groups: admins: - members: ["@admin:matrix-af2f3d.organic-machine.com"] + members: + - "@admin:matrix-af2f3d.organic-machine.com" + - "@egutierrez:matrix-af2f3d.organic-machine.com" everyone: members: ["*"]