package main import ( "encoding/json" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" ) // novox/hq issue 485: the mesh records every send — sender, lease epoch, generation, modules — and only `plan` // said it. `status` says per machine when it was last sent a declaration and by whom, in one line; `node show` // says the last send in full and what the machine answered; and `status --json` carries it as fields, since // the store's reader may not read the controller's tables and these verbs are the only window onto them. func aLastSend(answer inventory.SendAnswer) inventory.Send { sent := time.Date(2026, 10, 11, 18, 40, 5, 0, time.Local) return inventory.Send{NodeName: "laptop", Sequence: 12, Epoch: 57, Sender: "a one-shot push by jochen at a shell on anchor", Generation: 40, Digest: "d12", Modules: []string{"docker", "sudo"}, SentAt: sent, Recorded: true, Answer: answer} } func TestStatusSaysEachMachinesLastSendInOneLine(t *testing.T) { at := time.Date(2026, 10, 11, 18, 41, 0, 0, time.Local) asked := answers{ nodes: []inventory.Node{{Name: "anchor", LastSeen: time.Now()}, {Name: "laptop", LastSeen: time.Now()}}, lastSent: map[string]inventory.Send{"laptop": aLastSend(inventory.SendAnswer{ Outcome: inventory.OutcomeApplied, At: &at})}, } shown := printed(t, func() error { return printStatus(asked) }) var laptop, anchor []string for _, line := range strings.Split(shown, "\n") { fields := strings.Fields(line) if len(fields) > 0 && fields[0] == "laptop" { laptop = append(laptop, line) } if len(fields) > 0 && fields[0] == "anchor" { anchor = append(anchor, line) } } if len(laptop) != 1 || !strings.Contains(laptop[0], "2026-10-11 18:40") || !strings.Contains(laptop[0], "by a one-shot push by jochen at a shell on anchor") || !strings.Contains(laptop[0], "generation 40") || !strings.Contains(laptop[0], "applied") { t.Fatalf("status does not say laptop's last send in one line:\n%s", shown) } if len(anchor) != 1 || !strings.Contains(anchor[0], "no send recorded") { t.Fatalf("status does not say anchor has no send recorded, in one line:\n%s", shown) } } func TestStatusSaysTheLastSendsCouldNotBeRead(t *testing.T) { asked := answers{nodes: []inventory.Node{{Name: "laptop", LastSeen: time.Now()}}, lastSentUnread: "the store went away"} if shown := printed(t, func() error { return printStatus(asked) }); !strings.Contains(shown, "the store went away") { t.Fatalf("a record of sends that could not be read is not said:\n%s", shown) } if asked.well() { t.Error("a mesh whose sends could not be read is called well") } } func TestNodeShowSaysTheLastSendInFull(t *testing.T) { at := time.Date(2026, 10, 11, 18, 41, 0, 0, time.Local) refused := strings.Join(lastSendLines(aLastSend(inventory.SendAnswer{Outcome: inventory.OutcomeRefused, Refused: "unknown field \"generation\"\nsecond line", At: &at})), "\n") for _, want := range []string{"sequence 12", "2026-10-11 18:40:05", "a one-shot push by jochen at a shell on anchor", "lease epoch 57", "generation 40", "docker, sudo", "refused", "unknown field \"generation\""} { if !strings.Contains(refused, want) { t.Errorf("node show's last send does not say %q:\n%s", want, refused) } } if applied := strings.Join(lastSendLines(aLastSend(inventory.SendAnswer{Outcome: inventory.OutcomeApplied, At: &at})), "\n"); !strings.Contains(applied, "applied it at 2026-10-11 18:41:00") { t.Errorf("an applied send is not said applied:\n%s", applied) } if waiting := strings.Join(lastSendLines(aLastSend(inventory.SendAnswer{})), "\n"); !strings.Contains(waiting, "not reported on it yet") { t.Errorf("a send not reported on is not said so:\n%s", waiting) } stale := aLastSend(inventory.SendAnswer{}) when := at stale.RefusedAt, stale.RefusedApplied = &when, 44 if s := strings.Join(lastSendLines(stale), "\n"); !strings.Contains(s, "refused it at 2026-10-11 18:41:00") || !strings.Contains(s, "generation 44") { t.Errorf("a send refused for its generation is not said so:\n%s", s) } } func TestStatusJSONCarriesEachMachinesLastSend(t *testing.T) { at := time.Date(2026, 10, 11, 18, 41, 0, 0, time.UTC) asked := answers{nodes: []inventory.Node{{Name: "anchor"}, {Name: "laptop"}}, lastSent: map[string]inventory.Send{"laptop": aLastSend(inventory.SendAnswer{Outcome: inventory.OutcomeRefused, Refused: "unknown field", At: &at})}} body, err := statusAsJSON(asked) if err != nil { t.Fatal(err) } var got struct { LastSent []map[string]any `json:"lastSent"` } if err := json.Unmarshal(body, &got); err != nil { t.Fatal(err) } if len(got.LastSent) != 1 { t.Fatalf("status --json carries %d last sends, want laptop's only:\n%s", len(got.LastSent), body) } s := got.LastSent[0] if s["node"] != "laptop" || s["sequence"] != float64(12) || s["epoch"] != float64(57) || s["generation"] != float64(40) || s["sender"] != "a one-shot push by jochen at a shell on anchor" || s["digest"] != "d12" || s["sentAt"] == nil || s["answer"] != "refused" || s["refused"] != "unknown field" || s["answeredAt"] == nil { t.Fatalf("laptop's last send reads %v", s) } if modules, _ := s["modules"].([]any); len(modules) != 2 || modules[0] != "docker" { t.Fatalf("the modules it named read %v", s["modules"]) } } // And through the store: a send recorded and refused by the machine is what `node show` and `status --json` // read back.