package main import ( "context" "encoding/json" "reflect" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // heldPublish is a bus that holds a plan-moved publish until it is let go, and keeps the body. type heldPublish struct { link.Bus let chan struct{} body chan []byte } func (h *heldPublish) PublishSeatEvent(_ context.Context, _, _ string, body []byte) error { <-h.let h.body <- body return nil } func aWalkWithSteps() inventory.Plan { built := time.Date(2026, 10, 19, 12, 0, 0, 0, time.UTC) return inventory.Plan{ID: "plan-1", Repository: "novox/one", Commit: "abc", State: "walking", Tier: 1, Created: built, Updated: built, Revision: 4, Tiers: [][]string{{"alpha"}, {"beta", "gamma"}}, Modules: map[string]*inventory.PlanModule{ "alpha": {State: "built", BuiltAt: &built, Commit: "abc", First: []string{"ace"}}, "beta": {State: "asked", Why: "waiting"}, "gamma": {}, }, Times: &inventory.PlanTimes{Started: &built}, WalkSaid: inventory.WalkStarted} } func TestAnAnnouncementIsThePlanAsItWasWhenSaidWhatTheWalkDoesNext(t *testing.T) { // 1260/AC1 p := aWalkWithSteps() bus := &heldPublish{let: make(chan struct{}), body: make(chan []byte, 1)} sayPlanMoved(t.Context(), bus, nil, p) // The walk goes on, writing its steps while the announcement is being made. for i := 0; i < 2000; i++ { step := p.Modules["beta"] step.State, step.Why = "built", "changed after the call" step.First = append(step.First[:0], "novox") p.Modules["delta"] = &inventory.PlanModule{State: "asked"} p.Tiers[1][0] = "changed" p.Times.Started = nil delete(p.Modules, "delta") } close(bus.let) var said inventory.Plan select { case body := <-bus.body: if err := json.Unmarshal(body, &said); err != nil { t.Fatal(err) } case <-time.After(10 * time.Second): t.Fatal("nothing was announced") } if said.Modules["beta"].State != "asked" || said.Modules["beta"].Why != "waiting" || said.Tiers[1][0] != "beta" || len(said.Modules) != 3 || said.Times == nil || said.Times.Started == nil { t.Errorf("the announcement carries the walk as it went on, not as it was: %+v", said) } } func TestAnAnnouncementOfAnUnchangedPlanIsTheBodyItWasBeforeTheCopy(t *testing.T) { // 1260/AC2 p := aWalkWithSteps() bus := &heldPublish{let: make(chan struct{}), body: make(chan []byte, 1)} close(bus.let) sayPlanMoved(t.Context(), bus, nil, p) var got []byte select { case got = <-bus.body: case <-time.After(10 * time.Second): t.Fatal("nothing was announced") } // What it was: the plan whole, its phases worked out, and what the walk says of itself. q := p q.Phases = q.WalkPhases(time.Now(), nil) want, err := json.Marshal(planMovedBody{Plan: q, walkSaid: sayWalk(t.Context(), nil, q)}) if err != nil { t.Fatal(err) } var a, b map[string]any if json.Unmarshal(got, &a) != nil || json.Unmarshal(want, &b) != nil { t.Fatal("a body is not JSON") } delete(a, "phases") // worked out from the time of the call delete(b, "phases") if !reflect.DeepEqual(a, b) { t.Errorf("the body changed:\n got %s\nwant %s", got, want) } if a["walk"] != "plan-1" { t.Errorf("the walk's own words are not in the body: %s", got) } }