diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 0fbe30d6..ffac6f5f 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -2,12 +2,15 @@ package index import ( "context" + "errors" + "fmt" "reflect" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -114,12 +117,17 @@ func toolchainValue(t *testing.T, language, version, recipe string) []byte { } func reportToolchain(t *testing.T, ix *Indexer, module, node, language, version string) { + t.Helper() + reportToolchainRecipe(t, ix, module, node, language, version, "1") +} + +func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, version, recipe string) { t.Helper() key, err := delivery.ToolchainKey(node, language) if err != nil { t.Fatal(err) } - if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, "1"), false); err != nil { + if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, recipe), false); err != nil { t.Fatal(err) } } @@ -172,6 +180,31 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 } } +// ADR 0603 §3 (D9): a source revision that changes the Go build module's recipe and a Go module builds the module at once, with +// the recipe the nodes run, and again once the agreement moves to the new one: the module never waits and never loses its +// fingerprint between the two, and its toolchain edge names the recipe each was computed with. +func TestARecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.T) { + ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")}) + for _, node := range []string{"novox", "ace"} { + reportToolchain(t, ix, "mesh-build-go", node, "go", "go1.27.1") + } + first, _ := moduleOf(t, mods, "avahi") + if first.FP2 == "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "1" { + t.Fatalf("with the running recipe: fp2 %q, graph %+v", first.FP2, graphOf(t, ix, "avahi")) + } + // The release carrying the new recipe is merged, but the nodes still run the old one: one node reporting the new recipe is + // no strict majority, so the fingerprint stays, nothing waits. + reportToolchainRecipe(t, ix, "mesh-build-go", "novox", "go", "go1.27.1", "2") + if held, _ := moduleOf(t, mods, "avahi"); held.FP2 != first.FP2 || held.None2 != "" { + t.Errorf("one node on the new recipe: fp2 %q none2 %q, want the fingerprint of the running recipe, %q", held.FP2, held.None2, first.FP2) + } + reportToolchainRecipe(t, ix, "mesh-build-go", "ace", "go", "go1.27.1", "2") + again, _ := moduleOf(t, mods, "avahi") + if again.FP2 == "" || again.FP2 == first.FP2 || again.None2 != "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "2" { + t.Errorf("the agreement moved: fp2 %q (was %q) none2 %q graph %+v, want a second fingerprint at recipe 2", again.FP2, first.FP2, again.None2, graphOf(t, ix, "avahi")) + } +} + func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) { // 1444/AC6 // Both reports are read before the module's first fingerprint is computed (a fingerprint computed before they // disagree is kept: TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith). @@ -258,6 +291,34 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if _, held := graph.vals["avahi"]; !held { t.Error("graph.avahi went with another module") } + // Review finding 3 (ADR 0603 §2): a graph state not on the bus yet (process.ErrNotYet), or one whose listing fails, never + // fails a recompute; the first writes nothing and says so, the second skips the deletion pass and says so. + graph.vals["ghost"], graph.revs["ghost"] = []byte("x"), 1 + var said []string + ix.Logf = func(format string, args ...any) { said = append(said, fmt.Sprintf(format, args...)) } + for _, row := range []struct { + name string + err error + say string + }{ + {"not on the bus yet", process.ErrNotYet, "not on the bus yet"}, + {"a listing that fails", errors.New("the bus is slow"), "are not deleted"}, + } { + said, graph.keysErr = nil, row.err + if err := ix.Recompute(context.Background()); err != nil { + t.Errorf("%s: the recompute failed: %v", row.name, err) + } + if _, held := graph.vals["ghost"]; !held || !strings.Contains(strings.Join(said, "\n"), row.say) { + t.Errorf("%s: ghost held %v, log %q, want it kept and %q said", row.name, held, said, row.say) + } + } + graph.keysErr = nil + if err := ix.Recompute(context.Background()); err != nil { + t.Fatal(err) + } + if _, held := graph.vals["ghost"]; held { + t.Error("graph.ghost is still held once the graph can be listed") + } a := agentBuild("build-1792587792086548572", agentSrcY, "2026-10-21T15:03:00Z") _ = ix.Builds.Put(a[1].([]byte)) if err := ix.Recompute(context.Background()); err != nil {