CI / test (push) Successful in 1m43s
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>
75 lines
2.5 KiB
Go
75 lines
2.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/mirror"
|
|
)
|
|
|
|
func TestTheMirrorRefusesEverythingButReads(t *testing.T) {
|
|
// Stated here rather than inherited from http.FileServer, so a handler
|
|
// added later cannot quietly make the mirror writable — which would turn
|
|
// it into a way to replace a model with another one that every node then
|
|
// fetches without complaint.
|
|
h := readOnly(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
for _, m := range []string{http.MethodGet, http.MethodHead} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(m, "/x.gguf", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("%s was refused: %d", m, rec.Code)
|
|
}
|
|
}
|
|
for _, m := range []string{http.MethodPut, http.MethodPost, http.MethodDelete, http.MethodPatch} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(m, "/x.gguf", nil))
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("%s was allowed: %d", m, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheFilenameIsTakenFromTheURLWhenNotGiven(t *testing.T) {
|
|
for _, c := range []struct{ url, want string }{
|
|
{"https://h/a/b/model.gguf", "model.gguf"},
|
|
{"https://h/model.gguf?download=true", "model.gguf"},
|
|
{"model.gguf", "model.gguf"},
|
|
} {
|
|
if got := mirror.LastPathElement(c.url); got != c.want {
|
|
t.Errorf("mirror.LastPathElement(%q) = %q, want %q", c.url, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAVersionAnswersHeadAsWellAsGet(t *testing.T) {
|
|
// A sharded model's catalog source is the version directory, and the serve
|
|
// path checks a source with HEAD before accepting it. If the manifest
|
|
// answered only GET, every multi-file model would be refused as missing.
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "llama", "v1"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"model-00001-of-00002.safetensors", "model-00002-of-00002.safetensors"} {
|
|
if err := os.WriteFile(filepath.Join(root, "llama", "v1", name), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
h := readOnly(withManifest(root, http.FileServer(http.Dir(root))))
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/llama/v1/", nil))
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("a version should answer HEAD; got %d", w.Code)
|
|
}
|
|
if got := w.Header().Get("Content-Type"); got != "application/json" {
|
|
t.Errorf("expected a manifest, got %q", got)
|
|
}
|
|
}
|