Files
admin e68a7af4d3 fix(agent): slice-3 follow-ups — keep run-status on config fail, selftest usage, contract golden (v0.3.1)
- collect: a per-guest GuestConfig failure preserves the ListLXC run-status (only
  spec dropped); empty status normalized to "unknown". Test asserts preserved
  "running" + nil spec.
- main: --selftest usage error now reads (want read|task|hub).
- contract: testdata/host-report.golden.json + TestHostReport_ContractMatchesGolden
  (field-name key-set check vs golden; byte-identical with the hub copy).
- version 0.3.0 -> 0.3.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:29:05 +02:00

114 lines
3.6 KiB
Go

package hub
import (
"context"
"errors"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)
func newTestNodeStatus() proxmox.NodeStatus {
var ns proxmox.NodeStatus
ns.CPU = 0.05 // → 5%
ns.Uptime = 86400
ns.LoadAvg = []string{"0.10", "0.20", "0.15"}
ns.Memory.Total = 16000000000
ns.Memory.Used = 4000000000
ns.RootFS.Total = 100000000000
ns.RootFS.Used = 20000000000
return ns
}
func TestCollect_HostAndGuests(t *testing.T) {
px := &fakePx{
node: "demo-felhom",
ns: newTestNodeStatus(),
lxc: []proxmox.Guest{
{VMID: 100, Name: "acme", Status: "running", MaxMem: 2147483648, MaxDisk: 21474836480},
},
cfg: map[int]proxmox.GuestConfig{100: {Cores: 2, Memory: 2048}},
}
c := NewCollector(px, fakeProber{status: "active"}, "demo-host-01", "0.3.0", quietLogger())
r, err := c.Collect(context.Background())
if err != nil {
t.Fatalf("Collect: %v", err)
}
if r.HostID != "demo-host-01" || r.AgentVersion != "0.3.0" {
t.Errorf("top-level wrong: %+v", r)
}
if r.Host.Node != "demo-felhom" || r.Host.CPUPercent != 5 {
t.Errorf("host = %+v", r.Host)
}
if r.Host.MemoryPercent != 25 || r.Host.DiskPercent != 20 {
t.Errorf("percents = mem %v disk %v", r.Host.MemoryPercent, r.Host.DiskPercent)
}
if len(r.Guests) != 1 {
t.Fatalf("guests = %d", len(r.Guests))
}
g := r.Guests[0]
if g.VMID != 100 || g.Status != "running" || g.Spec == nil {
t.Fatalf("guest = %+v", g)
}
if g.Spec.Cores != 2 || g.Spec.MemoryBytes != 2147483648 || g.Spec.DiskBytes != 21474836480 {
t.Errorf("spec = %+v", g.Spec)
}
if r.Cloudflared.Status != "active" {
t.Errorf("cloudflared = %q", r.Cloudflared.Status)
}
}
func TestCollect_GuestConfigFailureKeepsStatusOmitsSpec(t *testing.T) {
px := &fakePx{
node: "demo-felhom",
ns: newTestNodeStatus(),
lxc: []proxmox.Guest{
{VMID: 100, Name: "ok", Status: "running", MaxMem: 1 << 31, MaxDisk: 1 << 34},
{VMID: 200, Name: "bad", Status: "running"},
},
cfg: map[int]proxmox.GuestConfig{100: {Cores: 2}},
cfgErr: map[int]error{200: errors.New("config read failed")},
}
c := NewCollector(px, fakeProber{status: "active"}, "h", "0.3.1", quietLogger())
r, err := c.Collect(context.Background())
if err != nil {
t.Fatalf("a per-guest failure must NOT fail the whole report: %v", err)
}
if len(r.Guests) != 2 {
t.Fatalf("guests = %d", len(r.Guests))
}
// GuestConfig failed for vmid 200, but its run-status (from ListLXC) is known and
// must be PRESERVED — only the spec is dropped.
bad := r.Guests[1]
if bad.Status != "running" {
t.Errorf("status = %q, want preserved \"running\" (not forced to unknown)", bad.Status)
}
if bad.Spec != nil {
t.Errorf("spec = %+v, want nil (omitted on config failure)", bad.Spec)
}
}
func TestCollect_NodeStatusFailureIsHardError(t *testing.T) {
px := &fakePx{node: "n", nsErr: errors.New("proxmox down")}
c := NewCollector(px, fakeProber{status: "active"}, "h", "0.3.0", quietLogger())
if _, err := c.Collect(context.Background()); err == nil {
t.Fatal("NodeStatus failure must be a hard error (no useful report)")
}
}
func TestCollect_CloudflaredProbeErrorIsUnknown(t *testing.T) {
px := &fakePx{node: "n", ns: newTestNodeStatus()}
c := NewCollector(px, fakeProber{err: errors.New("no systemctl")}, "h", "0.3.0", quietLogger())
r, err := c.Collect(context.Background())
if err != nil {
t.Fatalf("cloudflared failure must not be fatal: %v", err)
}
if r.Cloudflared.Status != "unknown" {
t.Errorf("cloudflared = %q, want unknown", r.Cloudflared.Status)
}
// Empty collections still present as non-nil.
if r.Guests == nil || r.StorageTargets == nil || r.AuditTail == nil {
t.Error("empty collections must be non-nil")
}
}