The mirror was a directory and a discipline written down in a README. This makes the three operations that matter into commands, on the mirror host, where `llmgw keys` already sets the precedent of operator tooling that works on files rather than through the API. The gateway still never writes to the mirror. That property was chosen deliberately — a mirror writable by the thing reading from it is a way to replace a model with one every node then fetches without complaint — and a person running a command does not take it away. `serve` states it rather than inheriting it, so a handler added later cannot quietly make the mirror writable. `add` downloads to a temporary name and renames, for the reason the agent's fetcher does: a mirror is copied to every node, so a partial file here is a partial file everywhere. It verifies the length the server promised, reports a checksum — the mirror is the one place a corrupt file reaches every node rather than being caught once — and refuses to replace an existing version, which is the whole point of the layout. `unused` refuses to answer when it cannot read the catalog. An empty catalog and an unreachable one produce the same list, everything, and the obvious next step after that list is deletion. It compares by path rather than by spelling, because a base typed with a different scheme or a trailing slash would otherwise report a file in use as unused. `remove` checks the catalog first and says to retire the entry before deleting the file, because a node that has to refetch a deleted file cannot, and that failure arrives later, on the node, looking like something else. Verified end to end: Range requests answer 206, which is what makes an interrupted fetch on a node resume; writes answer 405; a traversal attempt answers 404. Note: internal/agent/supervisor's TestSidecarRunsBeforeEngine fails under full suite load on main as well as here — it asserts that one shell's echo lands before another's, which the supervisor does not promise. Everything else passes. Left alone rather than fixed in a commit about mirrors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
5.1 KiB
Go
165 lines
5.1 KiB
Go
package mirror
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func serving(t *testing.T, body string) string {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL + "/model.gguf"
|
|
}
|
|
|
|
func TestAddPlacesAFileInTheVersionedLayout(t *testing.T) {
|
|
root := t.TempDir()
|
|
got, err := Add(context.Background(), root, serving(t, "weights"),
|
|
Entry{Model: "qwen-14b", Version: "2026-09", File: "q4_k_m.gguf"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Entry.Bytes != 7 {
|
|
t.Errorf("size = %d", got.Entry.Bytes)
|
|
}
|
|
if got.SHA256 == "" {
|
|
t.Error("no checksum reported; a mirror is the one place a corrupt file reaches every node")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "qwen-14b", "2026-09", "q4_k_m.gguf")); err != nil {
|
|
t.Errorf("the file is not where the layout says: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddRefusesToReplaceAVersion(t *testing.T) {
|
|
// The whole discipline. A version that can be overwritten takes away the
|
|
// thing versioned paths are for: changing the catalog back and still
|
|
// having the old weights.
|
|
root := t.TempDir()
|
|
e := Entry{Model: "m", Version: "v1", File: "f.gguf"}
|
|
if _, err := Add(context.Background(), root, serving(t, "first"), e, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := Add(context.Background(), root, serving(t, "second"), e, nil)
|
|
if !errors.Is(err, ErrExists) {
|
|
t.Fatalf("overwriting a version returned %v", err)
|
|
}
|
|
b, _ := os.ReadFile(e.Path(root))
|
|
if string(b) != "first" {
|
|
t.Errorf("the original was replaced anyway: %q", b)
|
|
}
|
|
}
|
|
|
|
func TestATruncatedDownloadIsNotRenamedIntoPlace(t *testing.T) {
|
|
// A partial file here is a partial file on every node.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Length", "100")
|
|
_, _ = w.Write([]byte("short"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
root := t.TempDir()
|
|
e := Entry{Model: "m", Version: "v1", File: "f.gguf"}
|
|
if _, err := Add(context.Background(), root, srv.URL, e, nil); err == nil {
|
|
t.Fatal("a truncated download was accepted")
|
|
}
|
|
if _, err := os.Stat(e.Path(root)); !os.IsNotExist(err) {
|
|
t.Error("the truncated file was left where a catalog could point at it")
|
|
}
|
|
// And no .part left looking like a model.
|
|
got, _ := List(root)
|
|
if len(got) != 0 {
|
|
t.Errorf("list shows %+v after a failed download", got)
|
|
}
|
|
}
|
|
|
|
func TestPathElementsAreNamesNotPaths(t *testing.T) {
|
|
// A slash would put the file somewhere the layout does not describe, and
|
|
// List would never find it again.
|
|
root := t.TempDir()
|
|
_, err := Add(context.Background(), root, serving(t, "x"),
|
|
Entry{Model: "../escape", Version: "v", File: "f"}, nil)
|
|
if err == nil {
|
|
t.Error("a path was accepted as a name")
|
|
}
|
|
}
|
|
|
|
func TestListIgnoresAnythingNotInTheLayout(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "m", "v1"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range []string{"real.gguf", "half.gguf.part"} {
|
|
if err := os.WriteFile(filepath.Join(root, "m", "v1", f), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// A file somebody dropped at the top level, which is not a model.
|
|
if err := os.WriteFile(filepath.Join(root, "notes.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := List(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 || got[0].File != "real.gguf" {
|
|
t.Errorf("list = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUnusedComparesByPathNotBySpelling(t *testing.T) {
|
|
// A catalog written by hand and a URL built from a path differ in the ways
|
|
// people differ. Reporting a file as unused because somebody typed the
|
|
// base differently would be followed by somebody deleting it.
|
|
entries := []Entry{
|
|
{Model: "m", Version: "v1", File: "a.gguf"},
|
|
{Model: "m", Version: "v2", File: "a.gguf"},
|
|
}
|
|
wanted := []string{
|
|
// A different scheme and a trailing slash on the base, both of which
|
|
// name the same file.
|
|
"https://models.internal/m/v1/a.gguf",
|
|
}
|
|
got := Unused(entries, wanted, "http://models.internal/")
|
|
if len(got) != 1 || got[0].Version != "v2" {
|
|
t.Errorf("unused = %+v, want only v2", got)
|
|
}
|
|
}
|
|
|
|
func TestRemoveTakesTheEmptyDirectoriesWithIt(t *testing.T) {
|
|
// An empty directory left behind makes list show a model that is not
|
|
// there, which is the kind of small lie that costs an afternoon.
|
|
root := t.TempDir()
|
|
e := Entry{Model: "m", Version: "v1", File: "f.gguf"}
|
|
if _, err := Add(context.Background(), root, serving(t, "x"), e, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Remove(root, e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "m")); !os.IsNotExist(err) {
|
|
t.Error("an empty model directory was left behind")
|
|
}
|
|
}
|
|
|
|
func TestProgressIsReportedForALongDownload(t *testing.T) {
|
|
root := t.TempDir()
|
|
var calls int
|
|
_, err := Add(context.Background(), root, serving(t, strings.Repeat("x", 1<<20)),
|
|
Entry{Model: "m", Version: "v", File: "f"}, func(done, total int64) { calls++ })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls == 0 {
|
|
t.Error("a download reported no progress at all")
|
|
}
|
|
}
|