Files
Felis/internal/updater/topology_test.go
T
flyemoji 96b3cc901c feat(updater): wire updates.Run to a caller with PaperMC v3 release discovery
internal/updates is a pure, fakes-tested decision core with no production caller,
so nothing could produce its "版本号状态" report. Add internal/updater as that caller:

- topology: the fixed platform components and their user-set policies (felis-api
  and cloudflared Scheduled+manageable; k3s Notify, high-blast-radius single node;
  velocity Notify, off-cluster and unmanageable). Minecraft is pinned by ABSENCE,
  never force-tracked here, appended from the live fleet at runtime.
- PaperMC Fill v3 release source: the v2 API (api.papermc.io) was retired
  2026-07-01 and returns HTTP 410, so this targets fill.papermc.io/v3, sends the
  required non-generic User-Agent, and returns the newest STABLE version, filtering
  the -SNAPSHOT/rc prereleases the plan would otherwise suppress. Its test fixture
  is captured from the live v3 response shape (2026-07-04).
- RoutingSource: the single ReleaseSource updates.Run requires, dispatching
  velocity to PaperMC and returning errGitHubNotWired for the GitHub-backed
  components so they degrade to "latest unknown" honestly, never a fabricated one.
- Runner: gather current versions (seam) -> assemble Components -> updates.Run ->
  Report; report-only when notifier and applier are nil.

Verification boundary: the parse/plan/compose logic is unit-tested (httptest +
fakes, fixture grounded in the live v3 shape). Live network/TLS/User-Agent
enforcement, the GitHub Releases source, the concrete version gatherer, the
notifier and applier, and the felis update CLI/CronJob remain integration work,
enumerated in doc.go.
2026-07-04 23:41:46 +09:00

49 lines
1.9 KiB
Go

package updater
import (
"testing"
"felis.lolicon.best/internal/updates"
)
// TestTopologyEncodesPolicies pins the user's stated decisions: which components Felis
// tracks and how aggressively. A regression here would silently change what Felis is
// allowed to auto-apply.
func TestTopologyEncodesPolicies(t *testing.T) {
want := map[string]Spec{
"felis-api": {Name: "felis-api", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "felis/felis"},
"k3s": {Name: "k3s", Policy: updates.PolicyNotify, Manageable: false, Source: sourceGitHub, Coord: "k3s-io/k3s"},
"cloudflared": {Name: "cloudflared", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "cloudflare/cloudflared"},
"velocity": {Name: "velocity", Policy: updates.PolicyNotify, Manageable: false, Source: sourcePaperMC, Coord: "velocity"},
}
got := Topology()
if len(got) != len(want) {
t.Fatalf("Topology has %d specs, want %d", len(got), len(want))
}
for _, s := range got {
w, ok := want[s.Name]
if !ok {
t.Errorf("unexpected component %q in topology", s.Name)
continue
}
if s != w {
t.Errorf("component %q = %+v, want %+v", s.Name, s, w)
}
}
}
// TestTopologyPinsMinecraft proves the pin is expressed as ABSENCE: no component in
// the static topology is Pinned, because Minecraft is never force-tracked here — it
// is appended from the live fleet at runtime. And nothing off-cluster is proposed for
// self-apply: velocity must be non-manageable (Notify at most, never Apply).
func TestTopologyPinsMinecraft(t *testing.T) {
for _, s := range Topology() {
if s.Policy == updates.PolicyPinned {
t.Errorf("component %q is Pinned in the static topology; Minecraft pins belong to the runtime fleet, not here", s.Name)
}
if s.Name == "velocity" && s.Manageable {
t.Error("velocity is off-cluster and must be non-manageable (Notify only)")
}
}
}