CI / test (push) Successful in 1m36s
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>
114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestOnlyURLsAreChecked(t *testing.T) {
|
|
// A path is checked by the node that will use it, which is the only
|
|
// machine whose answer means anything — the control plane may not even
|
|
// have that filesystem. A bare model id is the engine's business.
|
|
for _, src := range []string{
|
|
"/var/lib/llmgw-models/x.gguf", "./x.gguf", "Qwen/Qwen2.5-7B-Instruct",
|
|
} {
|
|
if _, ok := httpSource(src); ok {
|
|
t.Errorf("%q was treated as a URL to check", src)
|
|
}
|
|
if _, err := checkSource(context.Background(), src); err != nil {
|
|
t.Errorf("checking %q returned %v; it should be skipped", src, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnHFFileIsCheckedAtTheURLTheFetcherWouldUse(t *testing.T) {
|
|
// Asking about a string that merely looks similar would be worse than not
|
|
// asking: it would pass while the download later fails.
|
|
got, ok := httpSource("hf:Qwen/Qwen2.5-Coder-3B-Instruct-GGUF/q4.gguf")
|
|
if !ok {
|
|
t.Fatal("an hf: file was not recognised")
|
|
}
|
|
want := "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct-GGUF/resolve/main/q4.gguf"
|
|
if got != want {
|
|
t.Errorf("hf: resolved to %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAnHFRepositoryIsNotChecked(t *testing.T) {
|
|
// vLLM and SGLang download whole repositories themselves; there is no one
|
|
// file to ask about.
|
|
if _, ok := httpSource("hf:Qwen/Qwen2.5-7B-Instruct"); ok {
|
|
t.Error("a repository was treated as a single file")
|
|
}
|
|
}
|
|
|
|
func TestAReachableSourceReportsItsSize(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodHead {
|
|
t.Errorf("the check used %s, not HEAD — it must not download the file", r.Method)
|
|
}
|
|
w.Header().Set("Content-Length", "1073741824")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
size, err := checkSource(context.Background(), srv.URL+"/model.gguf")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if size != 1<<30 {
|
|
t.Errorf("size = %d", size)
|
|
}
|
|
}
|
|
|
|
func TestAMissingFileSaysSoWithTheURL(t *testing.T) {
|
|
// The whole point: a typo becomes a sentence while the operator still has
|
|
// the file open, rather than a replica failing minutes later on a node
|
|
// with an error that names the node.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
_, err := checkSource(context.Background(), srv.URL+"/typo.gguf")
|
|
if err == nil {
|
|
t.Fatal("a 404 was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "typo.gguf") {
|
|
t.Errorf("the error does not name the URL: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAServerThatSendsNoLengthIsStillFine(t *testing.T) {
|
|
// A resumable fetch does not need the length in advance, and refusing here
|
|
// would reject a mirror that is merely terse.
|
|
// A server that does not know the length omits the header rather than
|
|
// sending a negative one, which is what a real mirror streaming from a
|
|
// pipe does.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
size, err := checkSource(context.Background(), srv.URL+"/x.gguf")
|
|
if err != nil {
|
|
t.Errorf("a terse server was rejected: %v", err)
|
|
}
|
|
if size != 0 {
|
|
t.Errorf("an unknown length came back as %d, want 0", size)
|
|
}
|
|
}
|
|
|
|
func TestTheNoteSaysWhereTheAnswerCameFrom(t *testing.T) {
|
|
// The control plane's network is not the nodes' network. The check catches
|
|
// typos and a mirror that is down; it is not proof the fetch will work,
|
|
// and the wording must not suggest it is.
|
|
note := sourceNote("http://models.internal/x.gguf", 1<<30)
|
|
if !strings.Contains(note, "from here") {
|
|
t.Errorf("the note overstates what was checked: %s", note)
|
|
}
|
|
}
|