Files
llmgw/internal/mirror/api_test.go
T
Ilker ManapandClaude Opus 5 744a283cf1 Keep the checksum that finishing the download used to take away
All() preferred the disk entry wholesale, and a directory listing has no
checksum in it. So the sha256 a fetch computed was reported while the bytes
were still arriving and vanished the moment the file landed — it disappeared
exactly when it became verifiable, which is the one moment it is for.

It is the field an operator compares against what upstream published. A mirror
is the single place in this system where a corrupted file is copied to every
node instead of being caught once.

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

329 lines
11 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)
}
}