Files
llmgw/internal/control/reconciler/fetch_test.go
T
Ilker ManapandClaude Opus 5 a35e8796d2
CI / test (push) Successful in 1m28s
Carry a download's progress to where somebody is looking at it
The agent has always logged a percentage as it fetches weights, with a comment
saying a replica pulling a hundred gigabytes and one that is wedged look
identical from the control plane. They still did: the log is on the node, and
the node is not where anyone is looking when a replica has been starting for
ten minutes.

Progress now rides on the shard status and appears under the replica in `ctl
replicas` — a line rather than a column, because most replicas are not
downloading anything and a column would be empty for all of them.

It is deliberately not a state transition. Treating each chunk as one would
re-mark the shard as starting on every report and reset the timer that decides
when a slow start has become a failed one, so an hour-long download would keep
a doomed replica alive indefinitely. It arrives on its own method for that
reason, and a real transition clears it — otherwise a finished download sits in
the listing at ninety-nine percent under a shard that has been serving for an
hour.

Watchers are keyed by source rather than by shard, because a fetch is
single-flight: eight shards starting together produce one download and all
eight are waiting on it.

No percentage is invented when the server sent no length. One that stops moving
is worse than a byte count that keeps rising, so the field is absent and the
listing says "so far".

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

71 lines
2.5 KiB
Go

package reconciler
import (
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/supervisor"
)
func TestProgressIsVisibleWithoutBeingATransition(t *testing.T) {
// The failure this replaces: a replica pulling a hundred gigabytes and one
// that is wedged look identical from the control plane — both are a shard
// that has been starting for ten minutes.
//
// And progress must not count as a state change. Treating each chunk as
// one would reset the timer that decides when a slow start has become a
// failed one, so a download would keep a replica alive forever.
h := newHarness(t, testModel(1), gpuNode("n1", 8))
h.r.Reconcile(t.Context())
reps := h.r.Replicas()
if len(reps) != 1 || len(reps[0].Shards) == 0 {
t.Fatalf("expected a placed replica with shards, got %+v", reps)
}
rep, shard := reps[0].ID, reps[0].Shards[0].ID
h.r.OnFetchProgress(rep, shard, "hf:org/repo/model.gguf", 3<<30, 12<<30)
got := h.r.Replicas()[0].Shards[0]
if got.Fetch == nil {
t.Fatal("the download is not visible on the shard")
}
if got.Fetch.BytesDone != 3<<30 || got.Fetch.BytesTotal != 12<<30 {
t.Errorf("progress came back as %+v", got.Fetch)
}
if p, ok := got.Fetch.Percent(); !ok || p != 25 {
t.Errorf("percent = %d (known %v), want 25", p, ok)
}
}
func TestAnUnknownTotalHasNoPercentage(t *testing.T) {
// A server that sends no Content-Length is common. An invented percentage
// that stops moving is worse than a byte count that keeps rising.
f := FetchProgress{Source: "https://x/y.gguf", BytesDone: 500}
if _, ok := f.Percent(); ok {
t.Error("a percentage was invented from an unknown total")
}
}
func TestATransitionClearsTheDownload(t *testing.T) {
// Left in place it would sit in the listing at ninety-nine percent under
// a shard that has been serving for an hour.
h := newHarness(t, testModel(1), gpuNode("n1", 8))
h.r.Reconcile(t.Context())
reps := h.r.Replicas()
rep, shard := reps[0].ID, reps[0].Shards[0].ID
h.r.OnFetchProgress(rep, shard, "hf:org/repo/model.gguf", 1, 2)
h.r.OnShardStatus(rep, shard, supervisor.Ready, "")
if got := h.r.Replicas()[0].Shards[0]; got.Fetch != nil {
t.Errorf("a finished shard still shows a download: %+v", got.Fetch)
}
}
func TestProgressForAReplicaThatIsGoneIsIgnored(t *testing.T) {
// A download that finishes after its replica was torn down should not
// resurrect anything or panic.
h := newHarness(t, testModel(1), gpuNode("n1", 8))
h.r.OnFetchProgress("no-such-replica", "no-such-shard", "x", 1, 2)
}