package ml import ( "context" "os" "testing" "time" ) // TestSdcliResolveBinary_NotFound verifica que SdcliResolveBinary retorna error // cuando no hay binario en PATH ni hint. Forzamos PATH="" para que LookPath // no encuentre nada, lo que hace el test determinista independientemente del // entorno del desarrollador. func TestSdcliResolveBinary_NotFound(t *testing.T) { t.Setenv("PATH", "") _, err := SdcliResolveBinary("") if err == nil { t.Fatal("expected error when sd not in PATH, got nil") } } // TestSdcliResolveBinary_Hint verifica que un hint valido (archivo ejecutable) // se resuelve con Source="config" sin necesidad de PATH. func TestSdcliResolveBinary_Hint(t *testing.T) { // Crear archivo temporal ejecutable que simula el binario sd. // El script simplemente sale con 0; --version devolvera string vacio // pero eso no es error (version es best-effort). f, err := os.CreateTemp("", "sd-test-*") if err != nil { t.Fatalf("creating temp file: %v", err) } defer os.Remove(f.Name()) f.Close() script := []byte("#!/bin/sh\necho 'sd-test 0.1'\n") if err := os.WriteFile(f.Name(), script, 0o755); err != nil { t.Fatalf("writing temp file: %v", err) } if err := os.Chmod(f.Name(), 0o755); err != nil { t.Fatalf("chmod temp file: %v", err) } bin, err := SdcliResolveBinary(f.Name()) if err != nil { t.Fatalf("SdcliResolveBinary(hint): %v", err) } if bin.Source != "config" { t.Fatalf("expected source=config, got %q", bin.Source) } if bin.Path != f.Name() { t.Fatalf("expected path=%q, got %q", f.Name(), bin.Path) } } // TestSdcliGenerate_RequiresBinary es un test de integracion que salta si el // binario sd no esta instalado en PATH. Si esta disponible, tambien requiere // el modelo SD Turbo en el vault para ejecutar una generacion real. func TestSdcliGenerate_RequiresBinary(t *testing.T) { bin, err := SdcliResolveBinary("") if err != nil { t.Skipf("sd binary not in PATH, skipping integration test: %v", err) } modelPath := "/home/lucas/vaults/imagegen_models/diffusers/sd-turbo/sd_turbo.safetensors" if _, err := os.Stat(modelPath); err != nil { t.Skipf("SD Turbo model not in vault (%s), skipping: %v", modelPath, err) } cfg := GenerationConfig{ Prompt: "a red apple", Seed: 42, Steps: 4, CfgScale: 1.0, Sampler: "euler_a", Width: 512, Height: 512, Model: ModelRef{ Name: "sd-turbo", ModelType: "sd15", Quantization: "fp16", Path: modelPath, }, } outPath := t.TempDir() + "/out.png" ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() var progressCalled bool res, err := SdcliGenerate(ctx, bin, cfg, outPath, func(p SdcliProgress) { progressCalled = true t.Logf("progress: step %d/%d (%.1f%%) %.2fit/s", p.Step, p.TotalSteps, p.Percent, p.ItPerSec) }) if err != nil { t.Fatalf("SdcliGenerate: %v", err) } if len(res.ImageBytes) == 0 { t.Fatal("expected non-empty image bytes") } if res.Format != "png" { t.Fatalf("expected format=png, got %q", res.Format) } if res.DurationMs <= 0 { t.Fatalf("expected positive duration_ms, got %d", res.DurationMs) } if res.Meta["backend"] != "sdcli" { t.Fatalf("expected meta.backend=sdcli, got %v", res.Meta["backend"]) } t.Logf("generated %d bytes in %dms (progress_called=%v)", len(res.ImageBytes), res.DurationMs, progressCalled) }