Files
llmgw/internal/mirror/client_test.go
T
Ilker ManapandClaude Opus 5 7e6fb0059a Let the control plane call the mirror instead of somebody SSHing to it
The mirror has had a management API since it was built, and the only thing
driving it was curl on the mirror's own machine. This is the other half: a
client, so the operations belong to whatever has a reason to perform them.

The mirror's refusals travel as their own sentence rather than a status code.
Putting the reason in the body was deliberate on the server side, and a client
that kept only the number would undo it — "the mirror said 400" sends somebody
to the wrong machine, "the source URL answered 404" sends them to the right one.

NextProtos is cleared for the same reason the mirror's server clears it. That
configuration is shaped for the agent stream, which pins h2; a transport
carrying a pinned protocol it is not set up to speak negotiates h2 and then
talks HTTP/1.1 over it, and fails with an error mentioning neither ALPN nor
the mirror.

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

149 lines
4.7 KiB
Go

package mirror
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/tlsconf"
)
// testClient puts a real API behind a real HTTP server and points a real client
// at it, because the thing worth testing is the round trip: a handler and a
// client that each look right on their own can still disagree about a field
// name or a status code, and nothing but the two together catches that.
func testClient(t *testing.T) (*Client, string) {
t.Helper()
a, root := testAPI(t)
srv := httptest.NewServer(a.Handler())
t.Cleanup(srv.Close)
c, err := NewClient(srv.URL, tlsconf.Config{}, 0)
if err != nil {
t.Fatal(err)
}
return c, root
}
func TestTheClientReadsWhatTheMirrorHolds(t *testing.T) {
c, root := testClient(t)
writeFile(t, filepath.Join(root, "llama", "2026-01", "model.gguf"), "weights")
got, err := c.List(t.Context())
if err != nil {
t.Fatal(err)
}
// The base comes back from the mirror rather than being assembled here: the
// catalog entry has to be spelled the way the mirror spells it, and two
// spellings of one address is what makes a file in use look unused.
if got.Base != "http://mirror:8081" {
t.Errorf("expected the mirror's own base, got %q", got.Base)
}
if len(got.Models) != 1 || got.Models[0].Model != "llama" {
t.Fatalf("expected the one model on disk, got %+v", got.Models)
}
if got.Models[0].State != Present {
t.Errorf("expected present, got %q", got.Models[0].State)
}
}
func TestFetchReturnsBeforeTheDownloadFinishes(t *testing.T) {
c, _ := testClient(t)
got, err := c.Fetch(t.Context(), FetchRequest{
Source: serving(t, "weights"), Model: "m", Version: "v",
})
if err != nil {
t.Fatal(err)
}
// Under way, not done. A caller that waited for the bytes would hold a
// connection open for an hour on a real model and time out in between.
if got.Status.State != Fetching && got.Status.State != Present {
t.Errorf("expected the download to be under way, got %q", got.Status.State)
}
if got.URL != "http://mirror:8081/m/v/model.gguf" {
t.Errorf("the answer should say where the catalog will point; got %q", got.URL)
}
waitFor(t, "the download to finish", func() bool {
st, err := c.Status(t.Context(), "m", "v")
return err == nil && st.Status.State == Present
})
}
func TestTheMirrorsReasonSurvivesTheHop(t *testing.T) {
c, _ := testClient(t)
_, err := c.Fetch(t.Context(), FetchRequest{Source: "http://x/model.gguf"})
// The whole point of the mirror putting its reason in the body is that the
// far end can show it. A client that kept only the status code would turn
// "model and version are required" into "the mirror said 400".
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected an APIError, got %v", err)
}
if apiErr.Status != http.StatusBadRequest {
t.Errorf("expected 400, got %d", apiErr.Status)
}
if !strings.Contains(apiErr.Error(), "version") {
t.Errorf("the reason should name what was missing; got %q", apiErr.Error())
}
}
func TestAnUnknownModelComesBackAsNotFound(t *testing.T) {
c, _ := testClient(t)
_, err := c.Status(t.Context(), "nope", "v1")
var apiErr *APIError
if !errors.As(err, &apiErr) || apiErr.Status != http.StatusNotFound {
t.Fatalf("expected a 404 APIError, got %v", err)
}
}
func TestRemoveReportsWhatWent(t *testing.T) {
c, root := testClient(t)
writeFile(t, filepath.Join(root, "m", "v", "part-1.safetensors"), "a")
writeFile(t, filepath.Join(root, "m", "v", "part-2.safetensors"), "b")
removed, err := c.Remove(t.Context(), "m", "v")
if err != nil {
t.Fatal(err)
}
if len(removed) != 2 {
t.Errorf("both files of the version should have gone; got %v", removed)
}
}
func TestHealthFailsWhenTheMirrorIsNotThere(t *testing.T) {
// A closed port, not a slow one: the page has to be able to say the mirror
// is unreachable rather than render an empty table that looks like a mirror
// holding nothing.
srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
addr := srv.URL
srv.Close()
c, err := NewClient(addr, tlsconf.Config{}, 0)
if err != nil {
t.Fatal(err)
}
if err := c.Health(context.Background()); err == nil {
t.Fatal("expected an error from a mirror that is not listening")
} else if !strings.Contains(err.Error(), addr) {
t.Errorf("the error should name the address that refused; got %q", err)
}
}
func TestAnAddressWithoutAHostIsRefusedAtConstruction(t *testing.T) {
// Caught here rather than at the first request: a typo in configuration
// should stop the control plane starting, not surface as a broken page
// hours later.
if _, err := NewClient("10.0.0.146:8082", tlsconf.Config{}, 0); err == nil {
t.Error("expected an address with no scheme to be refused")
}
}