package services import ( "os" "strings" "testing" ) // La regla de aprobación es lo que separa un asistente de una máquina de spam // con el dominio del cliente. Si alguien la afloja, esto tiene que fallar. func TestRequiereAprobacion(t *testing.T) { casos := []struct { nombre string interna bool tool string requiere bool }{ // Canal público: cualquiera escribe. Todo lo que sale, espera. {"público no puede mandar correo", false, "enviar_correo", true}, {"público no puede llamar una herramienta", false, "avisar_stock", true}, {"público no puede generar documentos", false, "generar_documento", true}, // Leer no saca nada del negocio. {"público sí puede buscar en el conocimiento", false, "buscar_conocimiento", false}, {"público sí puede leer la bandeja", false, "leer_bandeja", false}, // Canal interno: el dueño ya autorizó al escribirlo. Mandarlo a // aprobar su propio pedido es fricción sin seguridad. {"el dueño manda correo directo", true, "enviar_correo", false}, {"el dueño ejecuta herramientas directo", true, "avisar_stock", false}, } for _, c := range casos { if got := RequiereAprobacion(c.interna, c.tool); got != c.requiere { t.Errorf("%s: RequiereAprobacion(interna=%v, %q) = %v, quiero %v", c.nombre, c.interna, c.tool, got, c.requiere) } } } // Lo que el modelo le cuenta al cliente cuando algo queda esperando no puede // filtrar la cocina del negocio. func TestEncolarNoFiltraElProcesoInterno(t *testing.T) { b, err := os.ReadFile("umind_aprobacion_service.go") if err != nil { t.Fatal(err) } s := string(b) i := strings.Index(s, "func EncolarAccion") j := strings.Index(s[i:], "\nfunc ") cuerpo := s[i : i+j] nota := cuerpo[strings.Index(cuerpo, `"nota":`):] nota = nota[:strings.Index(nota, "\n")] for _, prohibido := range []string{"rechaz", "aprobación interna"} { if strings.Contains(strings.ToLower(nota), prohibido) { t.Errorf("la nota al cliente menciona %q: %s", prohibido, nota) } } if !strings.Contains(strings.ToLower(nota), "sin mencionar") { t.Error("la nota tiene que instruir al modelo a no mencionar el proceso interno") } } // El payload se ejecuta tal cual se aprobó: si al aprobar se le volviera a // preguntar al modelo, se aprobaría una cosa y saldría otra. func TestAccionAprobadaUsaElPayloadGuardado(t *testing.T) { b, err := os.ReadFile("umind_aprobacion_service.go") if err != nil { t.Fatal(err) } s := string(b) i := strings.Index(s, "func EjecutarAccionAprobada") j := strings.Index(s[i:], "\n// avisarAccionPendiente") cuerpo := s[i : i+j] if !strings.Contains(cuerpo, "accion.ArgumentosJSON") { t.Error("la ejecución tiene que partir de los argumentos guardados") } if strings.Contains(cuerpo, "callAI") || strings.Contains(cuerpo, "ProcessWidgetMessage") { t.Error("aprobar no puede volver a llamar al modelo: se aprobaría una cosa y saldría otra") } // Dos personas mirando la misma bandeja pueden aprobar a la vez. if !strings.Contains(cuerpo, "ReclamarAccion") { t.Error("falta el reclamo: sin él, dos aprobaciones simultáneas ejecutan dos veces") } } // La clasificación es lo que decide entre ejecutar, encolar y rechazar. Sin // ella, todo lo que no fuera lectura se encolaba —incluidos los nombres que el // modelo inventa—, y la bandeja del dueño se llenaba de solicitudes para // ejecutar herramientas que nunca existieron. func TestCategoriaTool(t *testing.T) { casos := map[string]string{ "buscar_conocimiento": "lectura", "leer_bandeja": "lectura", "programar_aviso": "interna", "cancelar_aviso": "interna", "listar_avisos": "interna", "crear_vigilancia": "interna", "listar_vigilancias": "interna", "enviar_correo": "accion", "generar_documento": "accion", } for tool, quiero := range casos { // agenteID 0: las tools con nombre fijo no consultan la base. if got := CategoriaTool(0, tool); got != quiero { t.Errorf("CategoriaTool(%q) = %q, quiero %q", tool, got, quiero) } } } // El camino completo desde un canal público, que es donde escribe cualquiera. func TestPuertaDeToolsDesdeCanalPublico(t *testing.T) { b, err := os.ReadFile("umind_agent_service.go") if err != nil { t.Fatal(err) } s := string(b) i := strings.Index(s, "func executeUmindTool") cuerpo := s[i:] cuerpo = cuerpo[:strings.Index(cuerpo, "\nfunc ejecutarHerramienta")] // La clasificación tiene que decidir ANTES de encolar. cat := strings.Index(cuerpo, "CategoriaTool") enc := strings.Index(cuerpo, "EncolarAccion") if cat < 0 || enc < 0 || cat > enc { t.Fatal("la clasificación tiene que ir antes de encolar") } // Un nombre inexistente se rechaza, no se encola. desc := cuerpo[strings.Index(cuerpo, `case "desconocida":`):strings.Index(cuerpo, `case "interna":`)] if strings.Contains(desc, "EncolarAccion") { t.Error("una herramienta desconocida no puede encolarse") } // Las internas se rechazan desde un canal público, no se encolan. inter := cuerpo[strings.Index(cuerpo, `case "interna":`):strings.Index(cuerpo, `case "accion":`)] if strings.Contains(inter, "EncolarAccion") { t.Error("las tools internas no pueden encolarse desde un canal público") } if !strings.Contains(inter, "!sesionInterna") { t.Error("falta el rechazo de tools internas en canal público") } }