ca78c17b29
gates / gates (push) Successful in 19s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package osupdate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
)
|
|
|
|
// R-868 (v0.144.0). THE NIGHT'S SHAPE (A5, demo-hp 2026-10-05 02:57 UTC): the wrapper finished six packages, the agent
|
|
// was kill -9-ed before it read the report; the hub never got it. Here: the wrapper's kept copy and the plan file are
|
|
// on disk, a NEW agent process starts — it must send exactly one `applied` report and delete both files.
|
|
// COMPANION RED-PROOF: drop the SendUnsent body (return 0) → "the hub got no report".
|
|
func TestR868_KilledPassIsReportedAtStart(t *testing.T) {
|
|
w := &fakeWrapper{t: t}
|
|
l, h := newLeg(t, w, &hub.WireOSUpdate{Ring: 0, Enabled: true})
|
|
ring := 0
|
|
kept := WrapperReport{Mode: "apply", Layer: LayerGuest, RunID: "20261005T025700Z", Trigger: "debug", Ring: &ring, VMID: 9201,
|
|
ReleaseID: "ring0-20261005T025700Z", HealthBefore: guestOK(), HealthAfter: guestOK(),
|
|
Upgraded: []Package{{Name: "libc6", Version: "u4"}, {Name: "openssl", Version: "u3"}}}
|
|
b, _ := json.Marshal(kept)
|
|
rp := reportFile(l.PlanDir, kept.RunID, LayerGuest, "apply")
|
|
pp := planFile(l.PlanDir, kept.RunID, LayerGuest, "apply")
|
|
must(t, os.WriteFile(rp, b, 0o600))
|
|
must(t, os.WriteFile(pp, []byte("{}"), 0o600))
|
|
if n := l.SendUnsent(context.Background()); n != 1 {
|
|
t.Fatalf("sent %d, want 1", n)
|
|
}
|
|
if len(h.reports) != 1 {
|
|
t.Fatalf("the hub got no report (or several): %+v", h.reports)
|
|
}
|
|
r := h.reports[0]
|
|
if r.Outcome != "applied" || !r.Healthy || r.Trigger != "debug" || r.RunID != kept.RunID || r.Ring != 0 || len(r.Upgraded) != 2 || r.VMID != 9201 {
|
|
t.Fatalf("report = %+v", r)
|
|
}
|
|
for _, p := range []string{rp, pp} {
|
|
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
|
t.Fatalf("%s still on disk after the hub got it", filepath.Base(p))
|
|
}
|
|
}
|
|
// a second start sends nothing again — no duplicate report
|
|
if n := l.SendUnsent(context.Background()); n != 0 || len(h.reports) != 1 {
|
|
t.Fatalf("sent again: %d, reports %d", n, len(h.reports))
|
|
}
|
|
}
|
|
|
|
// A normal pass: the hub gets ONE report per layer and the kept copies are gone after it (nothing resent later).
|
|
// COMPANION RED-PROOF: drop the os.Remove(rep.unsent) in finish → the next pass resends → "2 guest reports".
|
|
func TestR868_NormalPassLeavesNoCopyAndNoDuplicate(t *testing.T) {
|
|
w := &fakeWrapper{t: t, keep: true, applyRep: map[string]WrapperReport{LayerGuest: {Upgraded: []Package{{Name: "libc6", Version: "u4"}}}}}
|
|
l, h := newLeg(t, w, &hub.WireOSUpdate{Ring: 0, Enabled: true})
|
|
l.Run(context.Background(), 9201, "debug")
|
|
left, _ := filepath.Glob(filepath.Join(l.PlanDir, "report-*.json"))
|
|
if len(left) != 0 {
|
|
t.Fatalf("kept copies left after the hub got them: %v", left)
|
|
}
|
|
l.Run(context.Background(), 9201, "debug") // the next pass sends kept copies first
|
|
guest := 0
|
|
for _, r := range h.reports {
|
|
if r.Layer == LayerGuest && r.Outcome == "applied" {
|
|
guest++
|
|
}
|
|
}
|
|
if guest != 2 {
|
|
t.Fatalf("%d guest reports for 2 passes (a duplicate or a loss)", guest)
|
|
}
|
|
}
|
|
|
|
type failingHub struct{ n int }
|
|
|
|
func (h *failingHub) PostOSReport(context.Context, []byte) error {
|
|
h.n++
|
|
return errors.New("hub away")
|
|
}
|
|
|
|
// The hub away: the copy stays, and goes at the next chance.
|
|
func TestR868_HubAwayKeepsTheCopy(t *testing.T) {
|
|
w := &fakeWrapper{t: t, keep: true, applyRep: map[string]WrapperReport{LayerGuest: {Upgraded: []Package{{Name: "libc6", Version: "u4"}}}}}
|
|
l, _ := newLeg(t, w, &hub.WireOSUpdate{Ring: 0, Enabled: true})
|
|
l.Appliance = false
|
|
l.Hub = &failingHub{}
|
|
l.Run(context.Background(), 9201, "night")
|
|
left, _ := filepath.Glob(filepath.Join(l.PlanDir, "report-*.json"))
|
|
if len(left) != 2 { // ring 0: the guest step and the Docker step each kept one
|
|
t.Fatalf("the copies must stay while the hub is away: %v", left)
|
|
}
|
|
h := &fakeHub{}
|
|
l.Hub = h
|
|
if n := l.SendUnsent(context.Background()); n != 2 {
|
|
t.Fatalf("sent %d: %+v", n, h.reports)
|
|
}
|
|
for _, r := range h.reports {
|
|
if r.Trigger != "night" || r.Ring != 0 {
|
|
t.Fatalf("the kept report lost its ids: %+v", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A pass in progress holds the lock: the sender at start must not take that pass's copy (it would be sent twice).
|
|
func TestR868_RunningPassKeepsTheSenderOff(t *testing.T) {
|
|
w := &fakeWrapper{t: t}
|
|
l, h := newLeg(t, w, nil)
|
|
must(t, os.WriteFile(reportFile(l.PlanDir, "r1", LayerGuest, "apply"), []byte(`{"mode":"apply","layer":"guest"}`), 0o600))
|
|
unlock := l.lockPass(true)
|
|
if n := l.SendUnsent(context.Background()); n != 0 || len(h.reports) != 0 {
|
|
t.Fatalf("sent while a pass ran: %d", n)
|
|
}
|
|
unlock()
|
|
if n := l.SendUnsent(context.Background()); n != 1 {
|
|
t.Fatalf("not sent after the pass: %d", n)
|
|
}
|
|
}
|
|
|
|
func must(t *testing.T, err error) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|