internal/autogen/loradir_test.go
Rama main · réplica de lectura del repositorio original.
package autogen
import (
"os"
"path/filepath"
"testing"
)
// The resolution ladder: per-model override > per-category folder > fleet-wide
// folder > the model gguf's own directory.
func TestResolveLoraDir_ladder(t *testing.T) {
const modelPath = `D:/LLM/Models/flux1/flux1-dev-Q8_0.gguf`
full := Settings{
LoraDir: `D:/loras/all`,
LoraDirs: map[string]string{"image": `D:/loras/image`, "video": `D:/loras/video`},
}
tests := []struct {
name string
s Settings
ov string
category string
want string
}{
{"override wins over everything", full, `D:/loras/mine`, "image", `D:/loras/mine`},
{"image takes the image folder", full, "", "image", `D:/loras/image`},
{"video takes the video folder", full, "", "video", `D:/loras/video`},
{
"a category with no entry falls back to the fleet-wide folder",
Settings{LoraDir: `D:/loras/all`, LoraDirs: map[string]string{"image": `D:/loras/image`}},
"", "video", `D:/loras/all`,
},
{
"a blank entry is not a pin",
Settings{LoraDir: `D:/loras/all`, LoraDirs: map[string]string{"video": " "}},
"", "video", `D:/loras/all`,
},
{"nothing set falls back to the model's own dir", Settings{}, "", "image", `D:/LLM/Models/flux1`},
{
"a per-category folder alone still beats the model's own dir",
Settings{LoraDirs: map[string]string{"video": `D:/loras/video`}},
"", "video", `D:/loras/video`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := resolveLoraDir(tt.s, tt.ov, tt.category, modelPath); got != tt.want {
t.Fatalf("resolveLoraDir = %q, want %q", got, tt.want)
}
})
}
}
// A nil map must not panic on lookup, which is the shape every pre-existing
// generate file has.
func TestResolveLoraDir_nilMap(t *testing.T) {
got := resolveLoraDir(Settings{LoraDir: `D:/loras`}, "", "video", `D:/m/x.gguf`)
if got != `D:/loras` {
t.Fatalf("resolveLoraDir = %q, want D:/loras", got)
}
}
func TestLoraCategory(t *testing.T) {
if got := loraCategory(true); got != "video" {
t.Errorf("loraCategory(true) = %q, want video", got)
}
if got := loraCategory(false); got != "image" {
t.Errorf("loraCategory(false) = %q, want image", got)
}
// Both must be keys the settings API will accept.
for _, c := range []string{loraCategory(true), loraCategory(false)} {
if !IsCategory(c) {
t.Errorf("loraCategory produced %q, which IsCategory rejects", c)
}
}
}
// The settings patch is how the dashboard writes these, so a patched map must
// reach Settings intact.
func TestSettingsPatch_loraDirs(t *testing.T) {
s := Settings{LoraDir: `D:/old`}
dirs := map[string]string{"video": `D:/loras/video`}
(&SettingsPatch{LoraDirs: &dirs}).apply(&s)
if s.LoraDirs["video"] != `D:/loras/video` {
t.Fatalf("after patch: LoraDirs = %v", s.LoraDirs)
}
if s.LoraDir != `D:/old` {
t.Fatalf("patch clobbered the fleet-wide LoraDir: %q", s.LoraDir)
}
}
// Saving an unrelated settings section must not drop the per-category folders.
// They are a map, not a pointer, so this exercises MergeSettingsPatch's nil
// carry-forward for non-pointer fields.
func TestMergeSettingsPatch_carriesForwardLoraDirs(t *testing.T) {
prevDirs := map[string]string{"video": `D:/loras/video`}
prev := SettingsPatch{LoraDirs: &prevDirs}
vram := 24.0
got := MergeSettingsPatch(prev, SettingsPatch{TargetVramGB: &vram})
if got.LoraDirs == nil || (*got.LoraDirs)["video"] != `D:/loras/video` {
t.Fatalf("loraDirs dropped by an unrelated save: %v", got.LoraDirs)
}
// A non-nil map is still an explicit overwrite (the advanced section
// rewrites itself wholesale on every save).
empty := map[string]string{}
cleared := MergeSettingsPatch(prev, SettingsPatch{LoraDirs: &empty})
if cleared.LoraDirs == nil || len(*cleared.LoraDirs) != 0 {
t.Fatalf("explicit clear ignored: %v", cleared.LoraDirs)
}
}
// The dashboard writes one category at a time, so the helper must be a
// read-modify-write over the single LoraDirs map rather than a plain overwrite.
func TestUpsertSidecarLoraDir_setClearAndPreserve(t *testing.T) {
dir := t.TempDir()
gen := filepath.Join(dir, "generate.yaml")
if err := os.WriteFile(gen, []byte("settings:\n modelsRoot: D:/Models\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := UpsertSidecarLoraDir(gen, "image", `D:/loras/image`); err != nil {
t.Fatal(err)
}
// Setting a SECOND category must not drop the first.
if err := UpsertSidecarLoraDir(gen, "video", `D:/loras/video`); err != nil {
t.Fatal(err)
}
gf, err := LoadGenerateFile(gen, "")
if err != nil {
t.Fatal(err)
}
if gf.Settings.LoraDirs["image"] != `D:/loras/image` || gf.Settings.LoraDirs["video"] != `D:/loras/video` {
t.Fatalf("after two sets: %v", gf.Settings.LoraDirs)
}
// Clearing one leaves the other alone.
if err := UpsertSidecarLoraDir(gen, "image", ""); err != nil {
t.Fatal(err)
}
gf, err = LoadGenerateFile(gen, "")
if err != nil {
t.Fatal(err)
}
if _, ok := gf.Settings.LoraDirs["image"]; ok {
t.Fatalf("image not cleared: %v", gf.Settings.LoraDirs)
}
if gf.Settings.LoraDirs["video"] != `D:/loras/video` {
t.Fatalf("clearing image dropped video: %v", gf.Settings.LoraDirs)
}
}
// An unrelated settings section saving afterwards must not wipe the folders.
func TestUpsertSidecarLoraDir_survivesAnotherSectionSave(t *testing.T) {
dir := t.TempDir()
gen := filepath.Join(dir, "generate.yaml")
if err := os.WriteFile(gen, []byte("settings:\n modelsRoot: D:/Models\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := UpsertSidecarLoraDir(gen, "video", `D:/loras/video`); err != nil {
t.Fatal(err)
}
vram := 24.0
if err := UpsertSidecarSettings(gen, SettingsPatch{TargetVramGB: &vram}); err != nil {
t.Fatal(err)
}
gf, err := LoadGenerateFile(gen, "")
if err != nil {
t.Fatal(err)
}
if gf.Settings.LoraDirs["video"] != `D:/loras/video` {
t.Fatalf("a memory-section save dropped the LoRA folder: %v", gf.Settings.LoraDirs)
}
}