Test rows: a missing graph state fails no recompute (review finding 3); a recipe change builds a Go module again (D9)

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 17:21:07 +01:00
parent 1387b10e78
commit b4855f21c3
@@ -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 {