package services import ( "strings" "testing" ) // Registrar una factura dictando los datos no era posible: la única // herramienta de venta era adjuntar_factura, que exige un archivo pendiente. // Sin una alternativa, el modelo no tenía con qué guardar y el usuario recibía // un "guardado exitosamente" sobre algo que nunca se creó. func TestExisteHerramientaParaFacturaSinAdjunto(t *testing.T) { tools := agentTools() nombres := map[string]agentTool{} for _, tl := range tools { nombres[tl.Function.Name] = tl } crear, ok := nombres["crear_factura"] if !ok { t.Fatal("falta crear_factura: no habría forma de registrar una factura de venta sin archivo adjunto") } if _, ok := nombres["adjuntar_factura"]; !ok { t.Error("adjuntar_factura debe seguir existiendo para el caso con documento") } // La descripción tiene que distinguirlas, o el modelo elige la equivocada // y falla por falta de adjunto — que es justo el problema original. d := strings.ToLower(crear.Function.Description) if !strings.Contains(d, "sin archivo adjunto") { t.Error("la descripción debe dejar claro que no necesita adjunto") } if !strings.Contains(d, "adjuntar_factura") { t.Error("la descripción debe remitir a adjuntar_factura cuando sí hay documento") } // cliente_id y monto son obligatorios: sin ellos la factura no sirve. req := strings.Join(crear.Function.Parameters.Required, ",") for _, campo := range []string{"cliente_id", "monto"} { if !strings.Contains(req, campo) { t.Errorf("%q debería ser obligatorio en crear_factura", campo) } } } // El modelo puede informar éxito aunque la herramienta haya fallado — pasó con // una factura que nunca se creó. El aviso lo escribe el código a partir del // resultado real, así que no depende de que el modelo se porte bien. func TestErrorDeTool(t *testing.T) { casos := []struct { nombre string resultado string esperado string }{ {"error de la herramienta", `{"error": "cliente_id requerido"}`, "cliente_id requerido"}, {"resultado exitoso", `{"ok": true, "factura_id": 12}`, ""}, {"lista de datos", `{"items": [], "total": 0}`, ""}, {"resultado no JSON", `algo suelto`, ""}, {"error vacío no cuenta", `{"error": ""}`, ""}, } for _, cas := range casos { if got := errorDeTool(cas.resultado); got != cas.esperado { t.Errorf("%s: errorDeTool(%s) = %q, esperaba %q", cas.nombre, cas.resultado, got, cas.esperado) } } } // El id explícito manda: si el modelo ya lo tiene, no hay que ir a la base. // Es el único camino del resolvedor que se puede probar sin datos. func TestResolverClienteIDPrefiereElIDExplicito(t *testing.T) { id, err := ResolverClienteID(42, "cualquier nombre") if err != nil || id != 42 { t.Fatalf("= (%d, %v), esperaba (42, nil)", id, err) } } // Sin id ni nombre no se puede adivinar: mejor un error claro que buscar en la // base con la cadena vacía y traer un cliente al azar. func TestResolverClienteIDSinDatos(t *testing.T) { if _, err := ResolverClienteID(0, " "); err == nil { t.Fatal("esperaba error cuando no hay ni id ni nombre") } } // Las herramientas que piden cliente deben aceptar también el nombre: es lo // que elimina el ciclo listar_clientes → leer → extraer id, que es donde se // perdía la factura. func TestHerramientasDeClienteAceptanNombre(t *testing.T) { for _, tl := range agentTools() { props := tl.Function.Parameters.Properties if _, pide := props["cliente_id"]; !pide { continue } if _, acepta := props["cliente_nombre"]; !acepta { t.Errorf("%s pide cliente_id pero no acepta cliente_nombre: obliga al modelo a resolver el id por su cuenta", tl.Function.Name) } } }