package view import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" ) const ( hA = "0123456789abcdef0123456789abcdef01234567" hB = "89abcdef0123456789abcdef0123456789abcdef" fF = "src1:1111111111111111111111111111111111111111111111111111111111111111" fG = "src1:2222222222222222222222222222222222222222222222222222222222222222" ) func candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) Entry { t.Helper() heads := make([]delivery.Head, len(pulls)) for i, p := range pulls { heads[i] = p.Head } hash, err := delivery.HeadsHash(heads) if err != nil { t.Fatal(err) } b, err := delivery.EncodeCandidate(delivery.Candidate{Group: group, HeadsHash: hash, Pulls: pulls}) if err != nil { t.Fatal(err) } return Entry{Bucket: delivery.CandidatesBucket, Key: group, Value: b, At: t0} } func pullOf(repo string, n int, head string, modules ...string) delivery.CandidatePull { if modules == nil { modules = []string{} } return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, Modules: modules} } func modEntry(t *testing.T, name string, m delivery.Module) Entry { t.Helper() b, err := delivery.EncodeModule(m) if err != nil { t.Fatal(err) } return Entry{Bucket: delivery.ModulesBucket, Key: name, Value: b, At: t0} } // trunkBuild is a build record as the controller writes it (mesh-sdk go/delivery/testdata/build-record-build-agent.json is // the captured shape: id, state, repository, ref, module, commit, path, source-fingerprint, source-inputs, at). func trunkBuild(module, id, ref, state, fp, at string) []Entry { rec := `{"id":"` + id + `","state":"` + state + `","repository":"novox/mesh-delivery","ref":"` + ref + `","module":"` + module + `","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/` + module + `","at":"` + at + `"` if fp != "" { rec += `,"source-fingerprint":"` + fp + `"` } rec += `}` return []Entry{ {Bucket: BuildsBucket, Key: module + ".build." + id, Value: []byte(rec), At: t0}, {Bucket: BuildsBucket, Key: module + ".newest", Value: []byte(id), At: t0}, } } func fpRow(t *testing.T, s Snapshot, module string) FingerprintRow { t.Helper() for _, f := range s.Fingerprints { if f.Module == module { return f } } t.Fatalf("no fingerprint row for %s in %+v", module, s.Fingerprints) return FingerprintRow{} } func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1 m := New() fill(m, candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a", "b"), pullOf("novox/mesh-catalog", 300, hB, "c")), candidate(t, "feat/y", pullOf("novox/mesh-delivery", 13, hB))) g := m.Snapshot().Groups if len(g) != 2 || g[0].Group != "feat/x" || g[1].Group != "feat/y" { t.Fatalf("groups = %+v", g) } // sorted by repository: mesh-catalog before mesh-delivery if len(g[0].Pulls) != 2 || g[0].Pulls[0].Repository != "novox/mesh-catalog" || g[0].Pulls[0].Number != 300 { t.Fatalf("pulls = %+v", g[0].Pulls) } p := g[0].Pulls[1] if p.Repository != "novox/mesh-delivery" || p.Number != 12 || p.Head != hA[:12] || strings.Join(p.Modules, ",") != "a,b" { t.Errorf("pull = %+v, want #12 with head %s and modules a,b", p, hA[:12]) } if len(g[1].Pulls) != 1 || len(g[1].Pulls[0].Modules) != 0 { t.Errorf("feat/y = %+v", g[1]) } m.Apply(Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Deleted: true}) if g := m.Snapshot().Groups; len(g) != 1 || g[0].Group != "feat/y" { t.Errorf("after the delete: %+v", g) } } func TestFingerprintAgainstTheNewestTrunkBuild(t *testing.T) { // 1241/AC2 m := New() fill(m, trunkBuild("a", "build-1", "main", "built", fF, "2026-10-19T10:00:00Z")...) fill(m, trunkBuild("d", "build-2", "main", "built", fF, "2026-10-19T10:00:00Z")...) fill(m, modEntry(t, "a", delivery.Module{FP: fF, Repository: "novox/mesh-delivery", Commit: hA}), modEntry(t, "d", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA}), modEntry(t, "b", delivery.Module{None: "an artifact compiles a Go program from a build source that git trees do not pin: no fingerprint before release 4", Repository: "novox/mesh-delivery", Commit: hA}), modEntry(t, "e", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA})) s := m.Snapshot() if r := fpRow(t, s, "a"); r.State != FPSame || r.Indexer != fF || r.Build != fF { t.Errorf("a = %+v, want same", r) } if r := fpRow(t, s, "d"); r.State != FPDiffers || r.Indexer != fG || r.Build != fF { t.Errorf("d = %+v, want differs with both", r) } if r := fpRow(t, s, "b"); r.State != FPNone || !strings.Contains(r.Reason, "release 4") || r.Indexer != "" { t.Errorf("b = %+v, want none with the reason", r) } if r := fpRow(t, s, "e"); r.State != FPNoBuild || r.Indexer != fG || r.Build != "" { t.Errorf("e = %+v, want no build with the indexer's alone", r) } } func TestOnlyTheNewestSuccessfulTrunkBuildCounts(t *testing.T) { // 1241/AC2 (the newest key is only a hint) m := New() fill(m, trunkBuild("a", "build-1", "main", "built", fG, "2026-10-19T09:00:00Z")...) fill(m, trunkBuild("a", "build-2", "main", "built", fF, "2026-10-19T10:00:00Z")...) fill(m, trunkBuild("a", "build-3", "main", "failed", fG, "2026-10-19T11:00:00Z")...) // newer, not built fill(m, trunkBuild("a", "build-4", "feat/x", "built", fG, "2026-10-19T12:00:00Z")...) // newer, not trunk; also the newest key fill(m, modEntry(t, "a", delivery.Module{FP: fF, Repository: "novox/mesh-delivery", Commit: hA})) if r := fpRow(t, m.Snapshot(), "a"); r.State != FPSame || r.Build != fF { t.Errorf("a = %+v, want same with the 10:00 trunk build's fingerprint", r) } } func TestBuildFingerprintIsReadWithTheSDKRecord(t *testing.T) { // 1241/AC3 // The record as the controller's encoder wrote it (mesh-sdk go/delivery/testdata/build-record-build-agent.json), whole, // with its source-inputs under their Go field names: the view reads source-fingerprint from it. const rec = `{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"}` const fp = "src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7" m := New() fill(m, Entry{Bucket: BuildsBucket, Key: delivery.BuildKey("build-agent", "build-1792395447088936520"), Value: []byte(rec), At: t0}, Entry{Bucket: BuildsBucket, Key: delivery.NewestKey("build-agent"), Value: []byte("build-1792395447088936520"), At: t0}, modEntry(t, "build-agent", delivery.Module{FP: fp, Repository: "novox/mesh-controller", Commit: hA})) if r := fpRow(t, m.Snapshot(), "build-agent"); r.State != FPSame || r.Build != fp { t.Errorf("build-agent = %+v, want same with %s", r, fp) } } func shown(t *testing.T) *Model { m := New() fill(m, trunkBuild("a", "build-1", "main", "built", fF, "2026-10-19T10:00:00Z")...) fill(m, candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a"), pullOf("novox/mesh-catalog", 300, hB, "c")), modEntry(t, "a", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA}), modEntry(t, "b", delivery.Module{None: "no tree", Repository: "novox/mesh-delivery", Commit: hA})) return m } func TestTableAndPagePrintTheGroupsAndTheComparison(t *testing.T) { // 1241/AC6 m := shown(t) var out bytes.Buffer Table(&out, m.Snapshot()) for _, want := range []string{"CANDIDATES", "feat/x", "novox/mesh-delivery#12", hA[:12], "novox/mesh-catalog#300", "FINGERPRINT", "differs", "indexer " + fG, "build " + fF, "no tree"} { if !strings.Contains(out.String(), want) { t.Errorf("the table lacks %q:\n%s", want, out.String()) } } rec := httptest.NewRecorder() Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) for _, want := range []string{"CANDIDATES", "feat/x", "novox/mesh-delivery#12", hA[:12], "FINGERPRINTS", "differs", fG, fF, "no tree"} { if !strings.Contains(rec.Body.String(), want) { t.Errorf("the page lacks %q:\n%s", want, rec.Body.String()) } } } func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silently m := New() fill(m, Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0}, Entry{Bucket: delivery.ModulesBucket, Key: "a", Value: []byte("{"), At: t0}) s := m.Snapshot() if len(s.Unreadable) != 2 || len(s.Groups) != 0 || len(s.Fingerprints) != 0 { t.Errorf("snapshot = %+v", s) } }