Files
llmgw/internal/mirror/api_test.go
T
Ilker ManapandClaude Opus 5 ed530133b6
CI / test (push) Successful in 1m41s
Find a model in the registry instead of pasting a URL into a box
Mirroring anything meant knowing a repository existed, knowing which of its
thirty files were the weights, and assembling a resolve URL by hand. The mirror
page had a text box for that URL and no way to find one.

It now searches Hugging Face and lists a repository's weight files with their
sizes and published checksums, each with a button that fills the fetch form.

Only weight files. A repository is mostly a README and a tokenizer, and listing
those buries the four entries somebody came for.

The size shown is the LFS size, not the plain one. For an LFS-stored file —
every weights file — the plain field is the size of the pointer, so the other
one reports a gigabyte model as 133 bytes, and that is the number somebody
would then plan disk against.

The source URL is pinned to the commit the listing came from rather than to a
branch, so what the mirror fetches is what was on screen. A repository
re-uploaded under the same branch otherwise leaves two different sets of
weights with identical provenance and no way to tell them apart. The version
defaults to that revision for the same reason.

The registry's own failures are reported on their own line. It is somebody
else's server on the other side of the internet, and it being down says nothing
about the mirror — which is what the page is for and has to keep working
regardless. With no registry configured there is no search box at all, rather
than one that can only time out.

Gating decodes from a bool or a string, because the field is documented as the
first and is the second in practice: strict decoding fails the whole response,
so one gated model would hide the other nineteen in a page of results.

And a bug this made obvious: downloads were keyed by model and version, so the
second shard of a sharded model was refused as "already fetching" until the
first had finished. A fifteen-shard model had to be pulled one file at a time,
each waiting on the last. Keyed by file now — which is what the single-flight
was ever protecting, since the race is two writers on one .part. A version made
of several files reports as the worst of them: it is not present until every
shard is, and one shard failing makes the version unusable however many others
arrived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 15:18:57 +02:00

437 lines
15 KiB
Go

package mirror
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// testAPI is an API over an empty mirror, and the root it holds.
func testAPI(t *testing.T) (*API, string) {
t.Helper()
root := t.TempDir()
a := NewAPI(NewManager(root, discardLog()), "http://mirror:8081")
// A download outlives the request that started it — that is the point of
// the manager. So the temp directory must not be torn down while one is
// still writing into it. Registered after t.TempDir so it runs first.
t.Cleanup(func() {
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
all, err := a.m.All()
if err != nil {
return
}
busy := false
for _, st := range all {
busy = busy || st.State == Fetching
}
if !busy {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Error("a download was still running when the test ended")
})
return a, root
}
func call(t *testing.T, a *API, method, path, body string) (int, map[string]any) {
t.Helper()
var r io.Reader
if body != "" {
r = strings.NewReader(body)
}
req := httptest.NewRequest(method, path, r)
w := httptest.NewRecorder()
a.Handler().ServeHTTP(w, req)
var out map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatalf("%s %s returned %d with an unreadable body %q: %v",
method, path, w.Code, w.Body.String(), err)
}
return w.Code, out
}
// waitFor polls until the condition holds, because a download runs in its own
// goroutine and the test would otherwise be asserting on a race.
func waitFor(t *testing.T, what string, cond func() bool) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("timed out waiting for %s", what)
}
func TestFetchAnswersBeforeTheBytesArrive(t *testing.T) {
a, root := testAPI(t)
source := serving(t, "weights")
code, body := call(t, a, "POST", "/v1/models", `{
"source": "`+source+`", "model": "llama", "version": "2026-01-01"}`)
// 202 rather than 200: a caller that reads this as "done" points a catalog
// at a file that is still arriving.
if code != http.StatusAccepted {
t.Fatalf("expected 202 while the download runs, got %d: %v", code, body)
}
if got := body["url"]; got != "http://mirror:8081/llama/2026-01-01/model.gguf" {
t.Errorf("the response should say where the catalog will point; got %v", got)
}
waitFor(t, "the download to land on disk", func() bool {
_, err := os.Stat(filepath.Join(root, "llama", "2026-01-01", "model.gguf"))
return err == nil
})
}
func TestTheFilenameComesFromTheSourceWhenNotGiven(t *testing.T) {
a, _ := testAPI(t)
_, body := call(t, a, "POST", "/v1/models",
`{"source": "`+serving(t, "w")+`", "model": "m", "version": "v"}`)
if got := body["url"]; !strings.HasSuffix(got.(string), "/m/v/model.gguf") {
t.Errorf("the filename should be taken from the source URL; got %v", got)
}
}
func TestFetchRefusesWithoutAModelAndVersion(t *testing.T) {
a, _ := testAPI(t)
code, body := call(t, a, "POST", "/v1/models", `{"source": "http://x/model.gguf"}`)
if code != http.StatusBadRequest {
t.Fatalf("expected 400 without a model and version, got %d", code)
}
// The reason belongs in the body: a bare 400 turns a typo into an hour of
// finding out which field was wrong.
if !strings.Contains(body["error"].(string), "version") {
t.Errorf("the error should name what is missing; got %q", body["error"])
}
}
func TestAskingTwiceDoesNotStartTwoDownloads(t *testing.T) {
a, _ := testAPI(t)
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("weights"))
}))
defer srv.Close()
defer close(release)
body := `{"source": "` + srv.URL + `/model.gguf", "model": "m", "version": "v"}`
if code, _ := call(t, a, "POST", "/v1/models", body); code != http.StatusAccepted {
t.Fatalf("the first fetch should be accepted, got %d", code)
}
waitFor(t, "the first download to register", func() bool {
st, ok := a.m.Status("m", "v")
return ok && st.State == Fetching
})
// The second must join the first rather than open a rival writer on the
// same .part file.
code, second := call(t, a, "POST", "/v1/models", body)
if code != http.StatusAccepted {
t.Fatalf("asking again should report the running download, got %d: %v", code, second)
}
st := second["status"].(map[string]any)
if st["state"] != string(Fetching) {
t.Errorf("the second answer should describe the download already running; got %v", st["state"])
}
}
func TestAFailedFetchIsRememberedWithItsReason(t *testing.T) {
a, _ := testAPI(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "gone", http.StatusNotFound)
}))
defer srv.Close()
call(t, a, "POST", "/v1/models",
`{"source": "`+srv.URL+`/model.gguf", "model": "m", "version": "v"}`)
// Remembered rather than forgotten: a control plane waiting on a model that
// is never coming must be able to find out why.
waitFor(t, "the failure to be recorded", func() bool {
st, ok := a.m.Status("m", "v")
return ok && st.State == Failed
})
code, body := call(t, a, "GET", "/v1/models/m/v", "")
if code != http.StatusOK {
t.Fatalf("a failed fetch should still be reportable, got %d", code)
}
st := body["status"].(map[string]any)
if !strings.Contains(st["error"].(string), "404") {
t.Errorf("the recorded error should say what upstream answered; got %q", st["error"])
}
}
func TestStatusReadsTheDiskForModelsThisProcessDidNotFetch(t *testing.T) {
a, root := testAPI(t)
// A restart forgets every download. A model that is present must not be
// reported as unknown merely because this process did not put it there.
writeFile(t, filepath.Join(root, "m", "v", "model.gguf"), "weights")
code, body := call(t, a, "GET", "/v1/models/m/v", "")
if code != http.StatusOK {
t.Fatalf("a model on disk should be found, got %d: %v", code, body)
}
if st := body["status"].(map[string]any); st["state"] != string(Present) {
t.Errorf("expected present, got %v", st["state"])
}
}
func TestAnUnknownModelIsNotFound(t *testing.T) {
a, _ := testAPI(t)
if code, _ := call(t, a, "GET", "/v1/models/nope/v1", ""); code != http.StatusNotFound {
t.Errorf("expected 404 for a model the mirror does not hold, got %d", code)
}
}
func TestListReportsTheBaseSoCatalogsAgree(t *testing.T) {
a, root := testAPI(t)
writeFile(t, filepath.Join(root, "m", "v", "model.gguf"), "weights")
code, body := call(t, a, "GET", "/v1/models", "")
if code != http.StatusOK {
t.Fatalf("expected 200, got %d", code)
}
// Returned rather than assumed: a caller reconstructing the URL with a
// different spelling is what makes a file look unused when it is not.
if body["base"] != "http://mirror:8081" {
t.Errorf("the listing should say where nodes fetch from; got %v", body["base"])
}
if n := len(body["models"].([]any)); n != 1 {
t.Errorf("expected the one model on disk, got %d", n)
}
}
func TestRemoveTakesEveryFileOfAVersion(t *testing.T) {
a, root := testAPI(t)
// A model split across files is useless with one of them missing, so the
// version is the unit of removal, not the file.
writeFile(t, filepath.Join(root, "m", "v", "part-1.safetensors"), "a")
writeFile(t, filepath.Join(root, "m", "v", "part-2.safetensors"), "b")
code, body := call(t, a, "DELETE", "/v1/models/m/v", "")
if code != http.StatusOK {
t.Fatalf("expected 200, got %d: %v", code, body)
}
if n := len(body["removed"].([]any)); n != 2 {
t.Errorf("both files should have gone; removed %v", body["removed"])
}
if _, err := os.Stat(filepath.Join(root, "m")); !os.IsNotExist(err) {
t.Error("the emptied model directory should have gone with it")
}
}
func TestRemoveRefusesWhileADownloadIsRunning(t *testing.T) {
a, _ := testAPI(t)
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("weights"))
}))
defer srv.Close()
defer close(release)
call(t, a, "POST", "/v1/models",
`{"source": "`+srv.URL+`/model.gguf", "model": "m", "version": "v"}`)
waitFor(t, "the download to register", func() bool {
st, ok := a.m.Status("m", "v")
return ok && st.State == Fetching
})
// Deleting under a running fetch lets the file reappear when the download
// finishes, which looks like the delete silently failed.
code, body := call(t, a, "DELETE", "/v1/models/m/v", "")
if code != http.StatusConflict {
t.Fatalf("expected 409 while downloading, got %d: %v", code, body)
}
}
func TestRemovingSomethingAbsentIsNotFound(t *testing.T) {
a, _ := testAPI(t)
if code, _ := call(t, a, "DELETE", "/v1/models/m/v", ""); code != http.StatusNotFound {
t.Errorf("expected 404, got %d", code)
}
}
func TestHealthFailsWhenTheMirrorDirectoryIsGone(t *testing.T) {
a, root := testAPI(t)
if code, _ := call(t, a, "GET", "/v1/health", ""); code != http.StatusOK {
t.Fatalf("a readable mirror should be healthy, got %d", code)
}
// A mirror whose disk has gone away still accepts connections. A check that
// only proved the process was running would call that healthy right up
// until a node tried to fetch.
if err := os.RemoveAll(root); err != nil {
t.Fatal(err)
}
if code, _ := call(t, a, "GET", "/v1/health", ""); code != http.StatusServiceUnavailable {
t.Errorf("expected 503 once the directory is unreadable, got %d", code)
}
}
func writeFile(t *testing.T, path, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
// discardLog writes nothing, so a test that provokes a failure on purpose does
// not print an error line and make a passing run look broken.
func discardLog() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func TestTheChecksumSurvivesTheDownloadFinishing(t *testing.T) {
a, _ := testAPI(t)
call(t, a, "POST", "/v1/models",
`{"source": "`+serving(t, "weights")+`", "model": "m", "version": "v"}`)
waitFor(t, "the download to finish", func() bool {
st, ok := a.m.Status("m", "v")
return ok && st.State == Present
})
// The listing used to prefer the disk entry wholesale, and a directory has
// no checksum in it — so the sha256 vanished at the exact moment the file
// became verifiable. It is the one field an operator compares against what
// upstream published.
_, body := call(t, a, "GET", "/v1/models", "")
models := body["models"].([]any)
if len(models) != 1 {
t.Fatalf("expected the one model, got %v", models)
}
got := models[0].(map[string]any)
if got["sha256"] == nil || got["sha256"] == "" {
t.Errorf("the checksum should still be reported once the file is present; got %v", got)
}
}
func TestTwoShardsOfOneVersionDownloadTogether(t *testing.T) {
a, _ := testAPI(t)
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("weights"))
}))
defer srv.Close()
defer close(release)
for _, shard := range []string{"part-1.safetensors", "part-2.safetensors"} {
code, body := call(t, a, "POST", "/v1/models", `{
"source": "`+srv.URL+`/`+shard+`", "model": "llama", "version": "v1",
"file": "`+shard+`"}`)
// Keyed by version, the second shard was refused as already fetching
// until the first had finished — so a fifteen-shard model had to be
// pulled one shard at a time, each waiting on the last.
if code != http.StatusAccepted {
t.Fatalf("%s was refused with %d: %v", shard, code, body)
}
st := body["status"].(map[string]any)
if st["file"] != shard {
t.Errorf("expected %s to be its own download, got %v", shard, st["file"])
}
}
}
func TestAskingTwiceForTheSameFileStillJoinsTheOneRunning(t *testing.T) {
a, _ := testAPI(t)
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("weights"))
}))
defer srv.Close()
defer close(release)
body := `{"source": "` + srv.URL + `/f.gguf", "model": "m", "version": "v", "file": "f.gguf"}`
call(t, a, "POST", "/v1/models", body)
waitFor(t, "the first to register", func() bool {
st, ok := a.m.Status("m", "v")
return ok && st.State == Fetching
})
// Per file is still single-flight: two writers on one .part file is the
// thing the key exists to prevent.
code, second := call(t, a, "POST", "/v1/models", body)
if code != http.StatusAccepted {
t.Fatalf("expected the running download to be reported, got %d", code)
}
if st := second["status"].(map[string]any); st["state"] != string(Fetching) {
t.Errorf("expected the one already running, got %v", st["state"])
}
}
func TestAVersionIsOnlyPresentOnceEveryShardIs(t *testing.T) {
a, root := testAPI(t)
writeFile(t, filepath.Join(root, "llama", "v1", "part-1.safetensors"), "a")
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("b"))
}))
defer srv.Close()
defer close(release)
call(t, a, "POST", "/v1/models", `{"source": "`+srv.URL+`/part-2.safetensors",
"model": "llama", "version": "v1", "file": "part-2.safetensors"}`)
waitFor(t, "the second shard to register", func() bool {
st, _ := a.m.Status("llama", "v1")
return st.State == Fetching
})
// One shard on disk and one still arriving is not a version anybody can
// load, so the version reports the worse of the two.
_, body := call(t, a, "GET", "/v1/models/llama/v1", "")
if st := body["status"].(map[string]any); st["state"] != string(Fetching) {
t.Errorf("expected the version to read as fetching, got %v", st["state"])
}
}
func TestRemovingAVersionIsRefusedWhileAnyShardIsArriving(t *testing.T) {
a, root := testAPI(t)
writeFile(t, filepath.Join(root, "llama", "v1", "part-1.safetensors"), "a")
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
_, _ = w.Write([]byte("b"))
}))
defer srv.Close()
defer close(release)
call(t, a, "POST", "/v1/models", `{"source": "`+srv.URL+`/part-2.safetensors",
"model": "llama", "version": "v1", "file": "part-2.safetensors"}`)
waitFor(t, "the shard to register", func() bool {
st, _ := a.m.Status("llama", "v1")
return st.State == Fetching
})
// Deleting the shard that already landed while its sibling is still coming
// leaves a directory that reappears half-populated.
if code, _ := call(t, a, "DELETE", "/v1/models/llama/v1", ""); code != http.StatusConflict {
t.Errorf("expected 409 while a shard is arriving, got %d", code)
}
}