package hub import ( "context" "encoding/json" "testing" ) // R-54 ยง9 rule 6: the guest_net stanza must appear in a report built through the PRODUCTION collect // path, not only in a struct a test constructed. The v0.91.0 defect was exactly this gap โ€” a seam // with green tests and no caller. type fakeGuestNet struct{ st *GuestNetStatus } func (f fakeGuestNet) GuestNetStatus(context.Context) *GuestNetStatus { return f.st } func TestCollect_GuestNetOmittedWhenReporterNil(t *testing.T) { px := &fakePx{node: "n", ns: newTestNodeStatus()} c := NewCollector(px, fakeProber{status: "active"}, fakeObserver{}, nil, nil, nil, "h", "0.92.0", quietLogger()) r, err := c.Collect(context.Background()) if err != nil { t.Fatalf("Collect: %v", err) } if r.GuestNet != nil { t.Fatalf("no reporter wired โ†’ guest_net must be omitted, got %+v", r.GuestNet) } // And it must be absent from the WIRE, not merely nil in Go โ€” an always-present empty stanza // would make "watchdog not wired" indistinguishable from "watchdog found nothing". b, _ := json.Marshal(r) var m map[string]any if err := json.Unmarshal(b, &m); err != nil { t.Fatal(err) } if _, ok := m["guest_net"]; ok { t.Fatalf("guest_net key present on the wire with no reporter wired: %s", b) } } func TestCollect_GuestNetPopulatedWhenWired(t *testing.T) { px := &fakePx{node: "n", ns: newTestNodeStatus()} c := NewCollector(px, fakeProber{status: "active"}, fakeObserver{}, nil, nil, nil, "h", "0.92.0", quietLogger()) c.SetGuestNetReporter(fakeGuestNet{st: &GuestNetStatus{ CheckedAt: "2026-07-21T10:00:00Z", Guests: []GuestNetGuest{{ VMID: 9201, State: "healthy", Mode: "dhcp", IP: "192.168.0.104", HasRoute: true, DHClientAlive: true, CheckedAt: "2026-07-21T10:00:00Z", }}, }}) r, err := c.Collect(context.Background()) if err != nil { t.Fatalf("Collect: %v", err) } if r.GuestNet == nil || len(r.GuestNet.Guests) != 1 { t.Fatalf("guest_net stanza missing from a collected report: %+v", r.GuestNet) } // The wire keys are the contract the hub will read; pin the ones an operator diagnoses with. b, _ := json.Marshal(r) var m map[string]any if err := json.Unmarshal(b, &m); err != nil { t.Fatal(err) } gn, ok := m["guest_net"].(map[string]any) if !ok { t.Fatalf("guest_net missing or wrong shape on the wire: %s", b) } guests, ok := gn["guests"].([]any) if !ok || len(guests) != 1 { t.Fatalf("guest_net.guests wrong on the wire: %v", gn) } g := guests[0].(map[string]any) for _, key := range []string{"vmid", "state", "mode", "ip", "has_route", "dhclient_alive"} { if _, ok := g[key]; !ok { t.Fatalf("guest_net.guests[0] is missing the %q key: %v", key, g) } } if g["dhclient_alive"] != true { t.Fatalf("dhclient_alive must survive the round trip: %v", g) } }