A repository with neither GGUF nor safetensors (by the registry's summary or its tags) is hidden under "runs here", which is ticked by default, and counted with the architectures llama.cpp cannot load; unticked it is listed. An open repository's pickle files are not offered, only counted, and a repository of pickles only says to look for another version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
13 KiB
Go
299 lines
13 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"html"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/gguf"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/hf"
|
|
)
|
|
|
|
func ggufModel(id, arch string, params int64, created time.Time) hf.Model {
|
|
return hf.Model{ID: id, CreatedAt: created, LastModified: created,
|
|
GGUF: &hf.GGUFInfo{Total: params, Architecture: arch, ContextLength: 32768}}
|
|
}
|
|
|
|
func TestTheExplorerOpensOnTrendingModelsTheNodesCanRun(t *testing.T) {
|
|
// Opened from the menu, with nothing typed: what is current, in the
|
|
// shape this cluster runs, without a search to start it.
|
|
now := time.Now()
|
|
reg := &fakeRegistry{results: []hf.Model{
|
|
ggufModel("Qwen/Qwen3-8B-GGUF", "qwen3", 8.2e9, now.Add(-90*24*time.Hour)),
|
|
ggufModel("odd/Kev-GGUF", "ggmlc", 5e8, now.Add(-2*24*time.Hour)),
|
|
}}
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror?tab=explore").Body.String()
|
|
|
|
q := reg.query
|
|
if q.Sort != hf.SortTrending || q.Task != "text-generation" || !q.GGUF || q.Search != "" {
|
|
t.Errorf("asked the registry for %+v", q)
|
|
}
|
|
if !strings.Contains(body, "Qwen/Qwen3-8B-GGUF") || !strings.Contains(body, "8.2B") {
|
|
t.Errorf("the trending model is missing or unsized: %s", body)
|
|
}
|
|
// The kev case: listed, it invites a fetch the nodes cannot use.
|
|
if strings.Contains(body, "odd/Kev-GGUF") {
|
|
t.Error("a model llama.cpp cannot load is offered")
|
|
}
|
|
if !strings.Contains(body, "1 more is hidden") {
|
|
t.Error("the page should say something was hidden, and why")
|
|
}
|
|
}
|
|
|
|
func TestUntickingRunsHereShowsEverythingWithAVerdict(t *testing.T) {
|
|
reg := &fakeRegistry{results: []hf.Model{
|
|
ggufModel("odd/Kev-GGUF", "ggmlc", 5e8, time.Now()),
|
|
}}
|
|
// x=1 is the submitted form; "here" absent is the box unticked.
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror?x=1&view=downloads&task=any&size=9").Body.String()
|
|
|
|
q := reg.query
|
|
if q.GGUF || q.Sort != hf.SortDownloads || q.Task != "" || q.MaxParams != 9e9 {
|
|
t.Errorf("asked the registry for %+v", q)
|
|
}
|
|
if !strings.Contains(body, "odd/Kev-GGUF") || !strings.Contains(body, "ggmlc ✗") {
|
|
t.Errorf("the unrunnable model should be listed and marked: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestNewThisMonthLeavesOutOlderRepositories(t *testing.T) {
|
|
now := time.Now()
|
|
reg := &fakeRegistry{results: []hf.Model{
|
|
ggufModel("a/Old-GGUF", "llama", 1e9, now.Add(-200*24*time.Hour)),
|
|
ggufModel("a/Fresh-GGUF", "llama", 1e9, now.Add(-3*24*time.Hour)),
|
|
}}
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror?view=new").Body.String()
|
|
if strings.Contains(body, "a/Old-GGUF") || !strings.Contains(body, "a/Fresh-GGUF") {
|
|
t.Errorf("new this month shows the wrong repositories: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestOpeningARepositoryKeepsTheWayBack(t *testing.T) {
|
|
reg := &fakeRegistry{results: []hf.Model{ggufModel("a/B-GGUF", "llama", 1e9, time.Now())}}
|
|
mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg})
|
|
body := html.UnescapeString(mirrorGet(t, mux, "/admin/mirror?view=new&size=3").Body.String())
|
|
if !strings.Contains(body, "size=3") || !strings.Contains(body, "repo=a%2FB-GGUF") {
|
|
t.Fatalf("the row's link should keep the filters: %s", body)
|
|
}
|
|
|
|
reg.repo = hf.Repo{ID: "a/B-GGUF", Revision: "abc123def456"}
|
|
body = html.UnescapeString(mirrorGet(t, mux, "/admin/mirror?view=new&size=3&repo=a/B-GGUF").Body.String())
|
|
if !strings.Contains(body, "back to the list") || !strings.Contains(body, "view=new") {
|
|
t.Errorf("the repository page should lead back to the same listing: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAnAdapterIsNotListedAsAModel(t *testing.T) {
|
|
reg := &fakeRegistry{results: []hf.Model{
|
|
ggufModel("a/Some-LoRA-GGUF", "qwen2", 2e6, time.Now()),
|
|
ggufModel("a/Tiny-GGUF", "qwen2", 135e6, time.Now()),
|
|
}}
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror?tab=explore").Body.String()
|
|
if strings.Contains(body, "Some-LoRA") || !strings.Contains(body, "a/Tiny-GGUF") {
|
|
t.Errorf("adapters should be left out and small models kept: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestARepositoryShowsItsCard(t *testing.T) {
|
|
reg := &fakeRegistry{repo: hf.Repo{
|
|
ID: "Qwen/X-GGUF", Revision: "abc123def456", License: "apache-2.0",
|
|
BaseModel: []string{"Qwen/X"}, Card: "# X\n\nA **small** model. See [the license](LICENSE).",
|
|
Files: []hf.File{{Path: "x.gguf", Bytes: 10}},
|
|
}}
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror?repo=Qwen/X-GGUF").Body.String()
|
|
for _, want := range []string{"<strong>small</strong>", "license apache-2.0", "<code>Qwen/X</code>",
|
|
`href="https://huggingface.co/Qwen/X-GGUF/blob/abc123def456/LICENSE"`,
|
|
`href="https://huggingface.co/Qwen/X-GGUF"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %s", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestARepositoryOpensAsAFragmentBeneathItsRow(t *testing.T) {
|
|
reg := &fakeRegistry{repo: hf.Repo{
|
|
ID: "Qwen/X-GGUF", Revision: "abc123def456", Card: "A **small** model.",
|
|
Files: []hf.File{{Path: "x.gguf", Bytes: 10}},
|
|
}}
|
|
rec := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror/repo?repo=Qwen/X-GGUF")
|
|
body := rec.Body.String()
|
|
if rec.Code != 200 {
|
|
t.Fatalf("status %d: %s", rec.Code, body)
|
|
}
|
|
// The card and the fetch form, and not a page around them.
|
|
for _, want := range []string{"<strong>small</strong>", `action="/admin/mirror/fetch"`, `name="csrf"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("missing %s: %s", want, body)
|
|
}
|
|
}
|
|
for _, unwanted := range []string{"<html", "back to the list", "<h3>"} {
|
|
if strings.Contains(body, unwanted) {
|
|
t.Errorf("the fragment carries %s", unwanted)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAFragmentForARegistryThatFailsSaysSo(t *testing.T) {
|
|
reg := &fakeRegistry{err: errors.New("reaching the model registry: timeout")}
|
|
rec := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror/repo?repo=Qwen/X-GGUF")
|
|
if rec.Code != 502 || !strings.Contains(rec.Body.String(), "timeout") {
|
|
t.Errorf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAnImportanceMatrixIsNotOfferedAsAModel(t *testing.T) {
|
|
reg := &fakeRegistry{repo: hf.Repo{
|
|
ID: "a/X-GGUF", Revision: "abc123def456", Architecture: "qwen2",
|
|
Files: []hf.File{{Path: "x-q4_k_m.gguf", Bytes: 10}, {Path: "imatrix-x.gguf", Bytes: 5}},
|
|
}}
|
|
body := mirrorGet(t, mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg}),
|
|
"/admin/mirror/repo?repo=a/X-GGUF").Body.String()
|
|
if !strings.Contains(body, "importance matrix") {
|
|
t.Errorf("the imatrix should be marked as not a model: %s", body)
|
|
}
|
|
if strings.Count(body, `action="/admin/mirror/fetch"`) != 1 {
|
|
t.Errorf("only the model should have a fetch form: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAFetchFromTheListAnswersWithItsOutcomeAlone(t *testing.T) {
|
|
// Started from a repository opened in the list, a fetch answers the
|
|
// page's script with the outcome, not a whole new page that would close
|
|
// the repository and lose the list's place.
|
|
m := &fakeMirror{}
|
|
mux := mirrorPage(t, m, true)
|
|
send := func(form url.Values) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest("POST", "/admin/mirror/fetch", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.Header.Set("Accept", "application/json")
|
|
r.AddCookie(&http.Cookie{Name: SessionCookie, Value: "good"})
|
|
r.AddCookie(&http.Cookie{Name: csrfCookie, Value: "tok"})
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
w := send(url.Values{"csrf": {"tok"}, "source": {"http://up/x.gguf"}, "model": {"x"}, "version": {"v1"}, "force": {"1"}})
|
|
if w.Code != 200 || !strings.Contains(w.Body.String(), `"message":"started downloading x/v1`) {
|
|
t.Errorf("%d %s", w.Code, w.Body.String())
|
|
}
|
|
if strings.Contains(w.Body.String(), "<html") {
|
|
t.Error("a whole page came back")
|
|
}
|
|
w = send(url.Values{"csrf": {"tok"}, "model": {"x"}})
|
|
if w.Code != 422 || !strings.Contains(w.Body.String(), `"error"`) {
|
|
t.Errorf("a refusal: %d %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// noProbe answers every file as an unreadable one, for tests that are not
|
|
// about what the files are.
|
|
func TestMain(m *testing.M) {
|
|
kindOf = func(context.Context, string) (gguf.Header, error) {
|
|
return gguf.Header{}, errors.New("not probed in tests")
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestAdaptersAreListedForAModelAndToldFromModelsByTheirFiles(t *testing.T) {
|
|
prev := kindOf
|
|
t.Cleanup(func() { kindOf = prev })
|
|
kindOf = func(_ context.Context, url string) (gguf.Header, error) {
|
|
if strings.Contains(url, "adapter") {
|
|
return gguf.Header{Architecture: "qwen2", Type: "adapter"}, nil
|
|
}
|
|
return gguf.Header{Architecture: "qwen2"}, nil
|
|
}
|
|
reg := &fakeRegistry{
|
|
results: []hf.Model{{ID: "ggml-org/LoRA-Qwen2.5-Coder-7B-Instruct-F16-GGUF", Downloads: 900}},
|
|
repo: hf.Repo{ID: "ggml-org/LoRA-Qwen2.5-Coder-7B-Instruct-F16-GGUF", Revision: "776530df0000",
|
|
BaseModel: []string{"Qwen/Qwen2.5-Coder-7B-Instruct"},
|
|
Files: []hf.File{{Path: "lora-adapter-f16.gguf", Bytes: 726 << 20}, {Path: "merged-Q4_K_M.gguf", Bytes: 4 << 30}}},
|
|
}
|
|
mux := mirrorPageWith(t, &fakeMirror{}, reg, true)
|
|
|
|
// From a held model's card: its repository is looked up, and the
|
|
// adapters listed are those of the model it was made from.
|
|
body := mirrorGet(t, mux, "/admin/mirror?tab=explore&view=adapters&from=bartowski/Qwen2.5-Coder-7B-Instruct-GGUF").Body.String()
|
|
want := "base_model:adapter:Qwen/Qwen2.5-Coder-7B-Instruct"
|
|
if len(reg.query.Filters) != 1 || reg.query.Filters[0] != want {
|
|
t.Errorf("asked the registry for %v, expected %s", reg.query.Filters, want)
|
|
}
|
|
if !strings.Contains(body, "LoRA adapters made for <code>Qwen/Qwen2.5-Coder-7B-Instruct</code>") {
|
|
t.Error("the page does not say whose adapters these are")
|
|
}
|
|
if strings.Contains(body, `aria-label="Task"`) {
|
|
t.Error("task and size filters shown for adapters, which rarely carry either")
|
|
}
|
|
|
|
repo := mirrorGet(t, mux, "/admin/mirror?repo=ggml-org/LoRA-Qwen2.5-Coder-7B-Instruct-F16-GGUF").Body.String()
|
|
if !strings.Contains(repo, "LoRA adapter · qwen2") {
|
|
t.Error("the adapter file is not marked as one")
|
|
}
|
|
if !strings.Contains(repo, `<span class="opt">model · qwen2</span>`) {
|
|
t.Error("the merged file is not marked as a model")
|
|
}
|
|
}
|
|
|
|
func TestARepositoryWithNothingANodeLoadsIsNotListed(t *testing.T) {
|
|
// Pickles only, or ONNX: a node refuses the first and no engine here
|
|
// reads the second, so the row would only invite a fetch that fails.
|
|
now := time.Now()
|
|
pickles := hf.Model{ID: "someone/old-pytorch", CreatedAt: now, LastModified: now, Tags: []string{"pytorch"}}
|
|
st := hf.Model{ID: "Qwen/Qwen3-8B", CreatedAt: now, LastModified: now, Tags: []string{"safetensors"}}
|
|
reg := &fakeRegistry{results: []hf.Model{pickles, st, ggufModel("Qwen/Qwen3-8B-GGUF", "qwen3", 8.2e9, now)}}
|
|
mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg})
|
|
|
|
body := mirrorGet(t, mux, "/admin/mirror?tab=explore").Body.String()
|
|
if strings.Contains(body, "someone/old-pytorch") {
|
|
t.Error("a repository with no GGUF and no safetensors is listed")
|
|
}
|
|
if !strings.Contains(body, "Qwen/Qwen3-8B<") && !strings.Contains(body, "Qwen/Qwen3-8B\"") || !strings.Contains(body, "Qwen/Qwen3-8B-GGUF") {
|
|
t.Errorf("the loadable repositories should stay: %s", body)
|
|
}
|
|
if !strings.Contains(body, "neither GGUF nor safetensors") {
|
|
t.Error("the page should say why one was hidden")
|
|
}
|
|
// Unticked, everything is shown.
|
|
body = mirrorGet(t, mux, "/admin/mirror?x=1&view=downloads&task=any").Body.String()
|
|
if !strings.Contains(body, "someone/old-pytorch") {
|
|
t.Error("with the box unticked the repository should be listed")
|
|
}
|
|
}
|
|
|
|
func TestARepositorysPickleFilesAreNotOffered(t *testing.T) {
|
|
reg := &fakeRegistry{repo: hf.Repo{
|
|
ID: "a/Both", Revision: "abc123def456",
|
|
Files: []hf.File{{Path: "model.safetensors", Bytes: 10}, {Path: "pytorch_model.bin", Bytes: 10},
|
|
{Path: "config.json", Bytes: 1}},
|
|
}}
|
|
mux := mirrorPageWith(t, &fakeMirror{}, reg, true)
|
|
body := mirrorGet(t, mux, "/admin/mirror?repo=a/Both").Body.String()
|
|
if strings.Contains(body, "pytorch_model.bin") {
|
|
t.Error("the pickle file is offered")
|
|
}
|
|
if !strings.Contains(body, "model.safetensors") || !strings.Contains(body, "1 file is not shown") {
|
|
t.Errorf("the safetensors should be offered and the pickle counted: %s", body)
|
|
}
|
|
|
|
reg.repo = hf.Repo{ID: "a/OnlyPickle", Revision: "abc123def456",
|
|
Files: []hf.File{{Path: "model.ckpt", Bytes: 10}}}
|
|
body = html.UnescapeString(mirrorGet(t, mux, "/admin/mirror?repo=a/OnlyPickle").Body.String())
|
|
if strings.Contains(body, "/resolve/abc123def456/model.ckpt") || !strings.Contains(body, "only in a pickle format") {
|
|
t.Errorf("a repository of pickles only should say so and offer nothing: %s", body)
|
|
}
|
|
}
|