Two defects, both of the same kind: something the CLI knew and the rest of the system did not. A profile can be edited while a replica is running against the old one. The reconciler deliberately leaves it alone — churning a replica that is serving costs more than the imperfection — but there was no way to see it, so the cluster could disagree with its own configuration indefinitely and read as healthy. Profile now carries a fingerprint, a replica records the one it was placed with, and both the API and `ctl replicas` say "stale profile" when they differ. An empty fingerprint is not stale: it means a replica adopted from an older agent, which is unknown rather than wrong. Adding a model from the admin page skipped the source check `ctl model add` has done all along, so a typo or a mirror that is down became a replica failing on a node minutes later, with an error naming the node. The check moves out of the CLI into a package both use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
package sourcecheck
|
|
|
|
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 := Check(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 := Check(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 := Check(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 := Check(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)
|
|
}
|
|
}
|