internal/autogen/llmlora_test.go
Rama main · réplica de lectura del repositorio original.
package autogen
import (
"strings"
"testing"
)
func f64(v float64) *float64 { return &v }
func TestResolveLlmLoraDir_ladder(t *testing.T) {
const model = "D:/LLM/Models/qwen/qwen3-27b-Q5_K_M.gguf"
cases := []struct {
name string
s Settings
want string
}{
{"llm key wins", Settings{
LoraDirs: map[string]string{"llm": "D:/loras/llm", "image": "D:/loras/img"},
LoraDir: "D:/loras/all",
}, "D:/loras/llm"},
{"falls back to fleet-wide", Settings{
LoraDirs: map[string]string{"image": "D:/loras/img"},
LoraDir: "D:/loras/all",
}, "D:/loras/all"},
{"falls back to the model's own dir", Settings{}, "D:/LLM/Models/qwen"},
{"blank llm key is not a setting", Settings{
LoraDirs: map[string]string{"llm": " "},
LoraDir: "D:/loras/all",
}, "D:/loras/all"},
// The image key must never leak into the llama-server path: an SDXL LoRA
// is not loadable by a text model, and this is the whole reason the map
// is keyed by category rather than shared.
{"image key is not consulted", Settings{
LoraDirs: map[string]string{"image": "D:/loras/img"},
}, "D:/LLM/Models/qwen"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := resolveLlmLoraDir(c.s, model); got != c.want {
t.Fatalf("got %q, want %q", got, c.want)
}
})
}
}
func TestResolveLlmLoraDir_backslashModelPath(t *testing.T) {
// A Windows-written generate file carries backslashes; the fallback has to
// find the last separator anyway.
got := resolveLlmLoraDir(Settings{}, `D:\LLM\Models\qwen\q.gguf`)
if got != "D:/LLM/Models/qwen" {
t.Fatalf("got %q", got)
}
}
func TestResolveLoraPath(t *testing.T) {
cases := []struct{ dir, in, want string }{
{"D:/loras", "adapter.gguf", "D:/loras/adapter.gguf"},
{"D:/loras/", "adapter.gguf", "D:/loras/adapter.gguf"},
{"D:/loras", "sub/adapter.gguf", "D:/loras/sub/adapter.gguf"},
{"D:/loras", `sub\adapter.gguf`, "D:/loras/sub/adapter.gguf"},
// Absolute in every spelling a shared config might carry: an absolute
// entry must survive being read on a platform whose filepath.IsAbs
// disagrees, which is why isAbsLoraPath is hand-rolled.
{"D:/loras", "E:/other/adapter.gguf", "E:/other/adapter.gguf"},
{"D:/loras", `E:\other\adapter.gguf`, "E:/other/adapter.gguf"},
{"D:/loras", "/mnt/loras/adapter.gguf", "/mnt/loras/adapter.gguf"},
{"D:/loras", `\\nas\share\adapter.gguf`, "//nas/share/adapter.gguf"},
// No folder configured and no model dir: the name stands alone rather
// than becoming "/adapter.gguf".
{"", "adapter.gguf", "adapter.gguf"},
{"D:/loras", " ", ""},
}
for _, c := range cases {
if got := resolveLoraPath(c.dir, c.in); got != c.want {
t.Errorf("resolveLoraPath(%q, %q) = %q, want %q", c.dir, c.in, got, c.want)
}
}
}
func TestLlmLoraLines(t *testing.T) {
s := Settings{LoraDirs: map[string]string{"llm": "D:/loras/llm"}}
const model = "D:/LLM/Models/qwen/q.gguf"
if got := llmLoraLines(s, nil, model); got != nil {
t.Fatalf("nil override should emit nothing, got %v", got)
}
if got := llmLoraLines(s, &Override{}, model); got != nil {
t.Fatalf("a model with no adapters must emit nothing (existing configs stay byte-identical), got %v", got)
}
ov := &Override{Loras: []LoraRef{
{Path: "a.gguf"}, // no scale => plain --lora
{Path: "b.gguf", Scale: f64(0.7)}, // scaled
{Path: "c.gguf", Scale: f64(0)}, // 0 is REAL: loaded-but-inert, enabled per request
{Path: " "}, // half-typed editor row, skipped
{Path: "E:/elsewhere/d.gguf", Scale: f64(1)},
}}
got := llmLoraLines(s, ov, model)
want := []string{
`--lora "D:/loras/llm/a.gguf"`,
`--lora-scaled "D:/loras/llm/b.gguf" 0.7`,
`--lora-scaled "D:/loras/llm/c.gguf" 0`,
`--lora-scaled "E:/elsewhere/d.gguf" 1`,
}
if len(got) != len(want) {
t.Fatalf("got %d lines %v, want %d", len(got), got, len(want))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("line %d = %q, want %q", i, got[i], want[i])
}
}
}
// --lora is Additive: a user's own --lora in custom args must ADD to the
// model's adapters, not silently replace them the way every ReplaceAll flag
// does. Guards both halves: the generated lines survive, and ClearOwnedFields
// does not wipe Override.Loras.
func TestOwnedKnobs_loraIsNotOwned(t *testing.T) {
owned := OwnedKnobs([]string{"--lora", "D:/x/extra.gguf"})
if owned["loras"] {
t.Fatal("--lora must not mark the loras knob owned (it is Additive)")
}
// A ReplaceAll neighbour still behaves, so the change is scoped.
if !OwnedKnobs([]string{"-c", "4096"})["ctx"] {
t.Fatal("-c should still own ctx")
}
}
// --lora-scaled takes TWO value tokens. An under-counted flag leaves its scale
// stranded as a bare token, which the walk then treats as a positional.
func TestOwnedKnobs_loraScaledConsumesBothValues(t *testing.T) {
// Without ExtraValues the walk would stop on "0.5" and then read "-c" as a
// fresh flag anyway, so assert via a token the walk must NOT misread: put a
// value that looks like a flag in the scale slot's wake.
owned := OwnedKnobs([]string{"--lora-scaled", "D:/x/a.gguf", "-c", "--top-k", "20"})
if owned["ctx"] {
t.Fatal("-c sat in --lora-scaled's second value slot and must have been consumed, not read as a flag")
}
if !owned["topK"] {
t.Fatal("--top-k after the consumed pair should still be seen")
}
}
func TestFlagTable_loraSpellings(t *testing.T) {
for _, name := range []string{"--lora", "--lora-scaled"} {
d, ok := LookupFlag(name)
if !ok {
t.Fatalf("%s missing from the flag table", name)
}
if d.Repeat != Additive {
t.Errorf("%s must be Additive", name)
}
if d.Knob != "loras" {
t.Errorf("%s knob = %q, want loras", name, d.Knob)
}
}
if d, _ := LookupFlag("--lora-scaled"); d.ExtraValues != 1 {
t.Errorf("--lora-scaled takes FNAME SCALE, so ExtraValues must be 1, got %d", d.ExtraValues)
}
if d, _ := LookupFlag("--lora"); d.ExtraValues != 0 {
t.Errorf("--lora takes one value, got ExtraValues %d", d.ExtraValues)
}
}
// The emitted flags must be reachable from a real command render, not just from
// the helper: the wiring into buildCmdLines is the part that silently goes
// missing.
func TestBuildCmdLines_emitsLoras(t *testing.T) {
s := Settings{LoraDirs: map[string]string{"llm": "D:/loras/llm"}}
row := GgufRow{FullPath: "D:/LLM/Models/qwen/q.gguf"}
ov := &Override{Loras: []LoraRef{{Path: "a.gguf", Scale: f64(0.5)}}}
lines := buildCmdLines(s, Metadata{}, row, profile{}, 4096, 99, 0, "q8_0", "q8_0", false, ov)
joined := strings.Join(lines, "\n")
if !strings.Contains(joined, `--lora-scaled "D:/loras/llm/a.gguf" 0.5`) {
t.Fatalf("adapter flag missing from the rendered command:\n%s", joined)
}
// And a model without adapters must be unchanged.
if strings.Contains(strings.Join(buildCmdLines(s, Metadata{}, row, profile{}, 4096, 99, 0, "q8_0", "q8_0", false, &Override{}), "\n"), "--lora") {
t.Fatal("a model with no adapters emitted a --lora flag")
}
}