package index import ( "context" "encoding/json" "path" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" ) const merge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // record is a build record as the controller writes it (mesh-sdk go/delivery/testdata/build-record-build-agent.json is // the captured shape): SourceInputs under its Go field names, no json keys. inputs is that object's JSON. func record(t *testing.T, id, module, repo, path, ref, state, at, inputs string) (string, []byte) { t.Helper() // an outcome has no state key (mesh-controller internal/chain/records.go:57-59); state asked is a request raw := `{"id":"` + id + `",` switch state { case "": case "failed": // a failure is an outcome with failed set and no state (mesh-controller internal/chain/records.go:109) raw += `"failed":"the build failed",` default: raw += `"state":"` + state + `",` } raw += `"repository":"` + repo + `","ref":"` + ref + `","module":"` + module + `","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"` + path + `","at":"` + at + `"` if inputs != "" { raw += `,"source-fingerprint":"src1:ignored","source-inputs":` + inputs } raw += `}` var check delivery.BuildRecord if err := json.Unmarshal([]byte(raw), &check); err != nil { t.Fatal(err) } return delivery.BuildKey(module, id), []byte(raw) } func modulesIndexer(t *testing.T, recs ...[2]any) (*Indexer, *memStore) { t.Helper() mods := newMem() ix := &Indexer{Store: newMem(), Modules: mods, Builds: NewBuilds()} for _, r := range recs { ix.Builds.Put(r[0].(string), r[1].([]byte)) } return ix, mods } func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2]any { k, v := record(t, id, module, repo, path, ref, state, at, inputs) return [2]any{k, v} } // mergedMsg is a merged message of the repository with these module directories' trees, each with a manifest naming its // module, the directory's last element ("r" for the root), as the forge bridge sends it (one manifest per directory that // holds a module.json). func mergedMsg(repo string, trees map[string]string) delivery.Forge { manifests := map[string]string{} for dir := range trees { manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}` } return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests} } func nameOfDir(dir string) string { if dir == "" { return "r" } return path.Base(dir) } func moduleOf(t *testing.T, st *memStore, key string) (delivery.Module, bool) { t.Helper() v, ok := st.vals[key] if !ok { return delivery.Module{}, false } m, err := delivery.DecodeModule(v) if err != nil { t.Fatal(err) } return m, true } const aInputs = `{"Module":"a","Tree":"oldtree","Bases":["registry.example/base@sha256:aaa"],"Contexts":null,"Toolchains":{"code":"go sha256:ccc 0123456789abcdef"},"Unpinned":""}` func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value writes nothing) ix, mods := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs)) m := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree"}) _ = ix.Apply(context.Background(), m) rev, writes := mods.revs["a"], mods.writes _ = ix.Apply(context.Background(), m) if mods.revs["a"] != rev || mods.writes != writes { t.Errorf("the same merge wrote again (%d writes, was %d)", mods.writes, writes) } } func TestMergedMessageRemovesThePullFromItsGroupToo(t *testing.T) { // 1238/AC5 with 1239 (both effects of one message) ix, _ := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs)) cands := ix.Store.(*memStore) _ = ix.Apply(context.Background(), pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a")) _ = ix.Apply(context.Background(), mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree"})) if _, ok := cands.vals["feat/x"]; ok { t.Error("the merged pull request is still a candidate") } } func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *testing.T) { // 1606/AC1, AC2 ix, mods := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs), rec(t, "build-2", "r", "novox/mesh-delivery", "", "main", "", "2026-10-19T09:00:00Z", strings.Replace(aInputs, `"Module":"a"`, `"Module":"r"`, 1))) ctx := context.Background() if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree", "": "roottree"})); err != nil { t.Fatal(err) } for module, want := range map[string]string{"a": "modules/a", "r": delivery.RootPath} { if m, ok := moduleOf(t, mods, module); !ok || m.Path != want { t.Errorf("after the merge %s = %+v (%v), want path %q", module, m, ok, want) } } if err := ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil { t.Fatal(err) } if err := ix.Recompute(ctx); err != nil { t.Fatal(err) } if m, _ := moduleOf(t, mods, "a"); m.Path != "modules/a" || m.Src != 7 { t.Errorf("after the source and a recompute a = %+v, want path modules/a kept and Src 7", m) } raw := string(mods.vals["a"]) if !strings.Contains(raw, `"path":"modules/a"`) { t.Errorf("the stored value %s has no path", raw) } } func TestARootModuleHasTheDirectoryDot(t *testing.T) { // 1615/AC3: empty means not known, a root module is "." ix, mods := modulesIndexer(t, rec(t, "build-1", "r", "novox/mesh-sdk", "", "main", "", "2026-10-19T09:00:00Z", strings.Replace(aInputs, `"Module":"a"`, `"Module":"r"`, 1))) if err := ix.Apply(context.Background(), mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree"})); err != nil { t.Fatal(err) } if m, ok := moduleOf(t, mods, "r"); !ok || m.Path != delivery.RootPath { t.Errorf("module = %+v (%v), want path %q", m, ok, delivery.RootPath) } } func TestAMergeAddingAModuleDirectoryWritesItsValueWithNoBuildRecordAnywhere(t *testing.T) { // I1: issue 1851, ADR 0501 clause 6 ix, mods, _ := manifestsIndexer(t) // no build record of any module avahi := catalogueManifest(t, "avahi") f := withManifests(mergedMsg(catalogueRepo, map[string]string{"modules/avahi": "newtree"}), map[string]string{"modules/avahi": avahi}, map[string]map[string]string{"modules/avahi": {"go.sum": avahiGoSum}}) if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } m, ok := moduleOf(t, mods, "avahi") if !ok || m.Path != "modules/avahi" || m.Repository != catalogueRepo || m.Commit != merge || m.Manifest != delivery.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" { t.Fatalf("module = %+v (%v), want its directory, merge, manifest and inputs from the message alone", m, ok) } if m.FP != "" || m.None != "" { t.Errorf("module = %+v, want no value of today's form (I4: it came from the build records)", m) } if m.FP2 == "" && m.None2 == "" { t.Errorf("module = %+v, want a new-form fingerprint or its reason", m) } } func TestAMergeWritesOnlyTheModulesItsManifestsName(t *testing.T) { // I1 ix, mods := modulesIndexer(t, rec(t, "build-2", "c", "novox/mesh-catalog", "modules/c", "main", "", "2026-10-19T09:00:00Z", aInputs)) f := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t", "modules/b": "t"}) f.Trees["modules/c"] = "t" // a directory whose manifest the message does not carry if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } for name, want := range map[string]bool{"a": true, "b": true, "c": false} { if _, ok := mods.vals[name]; ok != want { t.Errorf("modules.%s written %v, want %v", name, ok, want) } } } func TestAModuleHeldOfAnotherRepositoryIsLeftAndSaid(t *testing.T) { // I1: a move between repositories is the operator's (ADR 0420 clause 7) ix, mods := modulesIndexer(t) var logs []string ix.Logf = func(format string, args ...any) { logs = append(logs, format) } ctx := context.Background() if err := ix.Apply(ctx, mergedMsg("novox/mesh-catalog", map[string]string{"modules/a": "t"})); err != nil { t.Fatal(err) } before := string(mods.vals["a"]) if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t2"})); err != nil { t.Fatal(err) } if string(mods.vals["a"]) != before || len(logs) == 0 { t.Errorf("modules.a = %s (was %s), logs %v: want it left, and said", mods.vals["a"], before, logs) } } func TestTwoDirectoriesNamingOneModuleWriteOneAndSayTheOther(t *testing.T) { // I1 ix, mods := modulesIndexer(t) var logs []string ix.Logf = func(format string, args ...any) { logs = append(logs, format) } f := mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t1", "other/a": "t2"}) if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } if m, ok := moduleOf(t, mods, "a"); !ok || m.Path != "modules/a" || len(logs) == 0 { t.Errorf("module = %+v (%v), logs %v: want the first directory by name, and the other said", m, ok, logs) } // Once held, the directory the bucket holds stays the module's. f = mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "t1", "aaa/a": "t3"}) if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } if m, _ := moduleOf(t, mods, "a"); m.Path != "modules/a" { t.Errorf("module = %+v, want the held directory kept", m) } }