package chain import ( "context" "encoding/json" "errors" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/broker" ) func at(h int) time.Time { return time.Date(2026, 10, 18, h, 0, 0, 0, time.UTC) } // **A build is kept whole and read back with its reuse key and what it published** (ADR 0408 §1): its module, // repository, commit, reuse key, and an artifact by digest or a package release by name and version. func TestABuildIsKeptWholeAndReadBackWithItsReuseKeyAndWhatItPublished(t *testing.T) { ctx := context.Background() c := InMemory(nil) artifact := Build{ID: "b1", Repository: "mesh-catalog", Module: "notes", Commit: "c1", ReuseKey: "rk-1", Published: &Published{Artifact: "sha256:aa"}, Asked: at(1), At: at(2), Made: []Artifact{{Name: "notes", Kind: "image", Reference: "sha256:aa"}}} pkg := Build{ID: "b2", Repository: "mesh-sdk", Module: "sdk", Commit: "c2", ReuseKey: "rk-2", Published: &Published{Package: &Package{Name: "mesh-sdk", Version: "1.4.0"}}} for _, b := range []Build{artifact, pkg} { if _, err := c.Builds.Create(ctx, b.ID, b); err != nil { t.Fatal(err) } } got, ok, err := c.Builds.Get(ctx, "b1") if err != nil || !ok { t.Fatalf("read b1: %v %v", ok, err) } if got.Value.Module != "notes" || got.Value.Repository != "mesh-catalog" || got.Value.Commit != "c1" || got.Value.ReuseKey != "rk-1" || got.Value.Published.Artifact != "sha256:aa" || len(got.Value.Made) != 1 { t.Fatalf("the build came back as %+v", got.Value) } got, _, _ = c.Builds.Get(ctx, "b2") if p := got.Value.Published; p == nil || p.Package == nil || p.Package.Name != "mesh-sdk" || p.Package.Version != "1.4.0" { t.Fatalf("the package came back as %+v", p) } if _, ok, _ := c.Builds.Get(ctx, "nope"); ok { t.Fatal("a key nobody wrote holds a build") } } // **A write against a revision that moved is refused and nothing is laid over it** (ADR 0408 §2). func TestAWriteAgainstAMovedRevisionIsRefusedAndNothingIsLaidOver(t *testing.T) { ctx := context.Background() c := InMemory(nil) first, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"}) if err != nil { t.Fatal(err) } if _, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes"}); !errors.Is(err, ErrExists) { t.Fatalf("a create of a key that holds a value answered %v", err) } second, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c2"}, first) if err != nil { t.Fatal(err) } // A writer that read the first revision writes now. if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "stale"}, first); !errors.Is(err, ErrMoved) { t.Fatalf("a write at a moved revision answered %v", err) } got, _, _ := c.Assignments.Get(ctx, "anchor.notes") if got.Value.Commit != "c2" || got.Revision != second { t.Fatalf("the key that moved was laid over: %+v at %d", got.Value, got.Revision) } } // **No key of the chain is ever removed** (ADR 0408 §4): the Backend has no delete and no purge, so nothing that holds // one can remove a key; and an assignment of a module taken off its node is kept, with its earlier values. func TestNoKeyOfTheChainIsEverRemoved(t *testing.T) { var backend Backend = NewMemory() if _, ok := backend.(interface { Delete(context.Context, string, uint64) error }); ok { t.Fatal("the backend can delete") } if _, ok := backend.(interface { Purge(context.Context, string) error }); ok { t.Fatal("the backend can purge") } if _, ok := any(InMemory(nil).Builds).(interface { Delete(context.Context, string) error }); ok { t.Fatal("a bucket of the chain can delete") } ctx := context.Background() c := InMemory(nil) rev, _ := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"}) if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"}, rev); err != nil { t.Fatal(err) } keys, err := c.Assignments.Keys(ctx) if err != nil || len(keys) != 1 || keys[0] != "anchor.notes" { t.Fatalf("keys %v %v", keys, err) } history, err := c.Assignments.History(ctx, "anchor.notes") if err != nil || len(history) != 2 { t.Fatalf("the key keeps %d values: %v", len(history), err) } } // **Each key keeps up to sixty-four earlier values** (ADR 0408 §5), oldest first. func TestAKeyKeepsItsEarlierValuesUpToSixtyFour(t *testing.T) { ctx := context.Background() c := InMemory(nil) rev, _ := c.Rollouts.Create(ctx, "r1", Rollout{ID: "r1", State: "n0"}) for i := 1; i <= 70; i++ { var err error if rev, err = c.Rollouts.Update(ctx, "r1", Rollout{ID: "r1", State: "n" + string(rune('A'+i%26))}, rev); err != nil { t.Fatal(err) } } history, err := c.Rollouts.History(ctx, "r1") if err != nil { t.Fatal(err) } if len(history) != broker.ChainHistory { t.Fatalf("the key keeps %d values and the bus keeps %d", len(history), broker.ChainHistory) } for i := 1; i < len(history); i++ { if history[i].Revision <= history[i-1].Revision { t.Fatal("the history is not oldest first") } } } // **A value over the largest message is refused, not cut, and the key is unchanged** (ADR 0408 §6). func TestAValueOverTheLargestMessageIsRefusedNotCut(t *testing.T) { ctx := context.Background() c := InMemory(nil) small := Build{ID: "b1", Repository: "r", Commit: "c"} rev, err := c.Builds.Create(ctx, "b1", small) if err != nil { t.Fatal(err) } huge := Build{ID: "b1", Repository: "r", Commit: "c", Manifest: json.RawMessage(`"` + strings.Repeat("x", 1<<20) + `"`)} if _, err := c.Builds.Update(ctx, "b1", huge, rev); !errors.Is(err, ErrTooLarge) { t.Fatalf("a value of a mebibyte answered %v", err) } if _, err := c.Builds.Create(ctx, "b2", huge); !errors.Is(err, ErrTooLarge) { t.Fatalf("a created value of a mebibyte answered %v", err) } got, _, _ := c.Builds.Get(ctx, "b1") if got.Revision != rev || len(got.Value.Manifest) != 0 { t.Fatalf("the key was changed: %+v", got) } if _, ok, _ := c.Builds.Get(ctx, "b2"); ok { t.Fatal("a refused create left a key") } // The largest that fits is written whole. fits := Build{ID: "b3", Manifest: json.RawMessage(`"` + strings.Repeat("x", broker.ChainValueBytes-1024) + `"`)} if _, err := c.Builds.Create(ctx, "b3", fits); err != nil { t.Fatalf("a value under the largest was refused: %v", err) } got, _, _ = c.Builds.Get(ctx, "b3") if len(got.Value.Manifest) != len(fits.Manifest) { t.Fatal("a value under the largest was cut") } } // **A rollout names every assignment it changed, with the build before and after** (ADR 0408 §1). func TestARolloutNamesEveryAssignmentItChangedWithBuildBeforeAndAfter(t *testing.T) { ctx := context.Background() c := InMemory(nil) r := Rollout{ID: "r1", State: "rolling", Commits: []Commit{{Repository: "mesh-catalog", Commit: "c2"}}, Tiers: [][]string{{"notes"}}, Links: Links{Builds: []string{"b2"}, Changed: []Change{ {Node: "anchor", Module: "notes", Before: Ref{Build: "b1", Commit: "c1"}, After: Ref{Build: "b2", Commit: "c2"}}, {Node: "g14", Module: "notes", After: Ref{Build: "b2", Commit: "c2"}}}}} if _, err := c.Rollouts.Create(ctx, "r1", r); err != nil { t.Fatal(err) } got, _, _ := c.Rollouts.Get(ctx, "r1") if len(got.Value.Changed) != 2 || got.Value.Changed[0].Before.Build != "b1" || got.Value.Changed[0].After.Build != "b2" || got.Value.Changed[1].Before.Build != "" || got.Value.Changed[1].After.Commit != "c2" { t.Fatalf("the rollout came back as %+v", got.Value) } // And in the contract's words: `changed`, with `before` and `after` naming `build` and `commit`. raw, _ := json.Marshal(got.Value) for _, want := range []string{`"changed":[`, `"before":{"build":"b1","commit":"c1"}`, `"after":{"build":"b2","commit":"c2"}`, `"builds":["b2"]`, `"commits":[`, `"tiers":[["notes"]]`} { if !strings.Contains(string(raw), want) { t.Errorf("the rollout's value lacks %s: %s", want, raw) } } } // **An assignment names its build, artifact or package, commit and rollout**, and keeps the fields its readers read // under the names they read them by (ADR 0408 §3). func TestAnAssignmentNamesBuildArtifactCommitAndRollout(t *testing.T) { a := Assignment{Node: "anchor", Module: "notes", Assigned: at(3), RequiredBy: "wiki", Provisioning: json.RawMessage(`{"plan":1}`), Build: "b2", Artifact: "sha256:bb", Commit: "c2", Rollout: "r1", Since: at(4)} raw, _ := json.Marshal(a) for _, want := range []string{`"node":"anchor"`, `"module":"notes"`, `"assigned":"2026-10-18T03:00:00Z"`, `"required-by":"wiki"`, `"provisioning":{"plan":1}`, `"build":"b2"`, `"artifact":"sha256:bb"`, `"commit":"c2"`, `"rollout":"r1"`, `"since":"2026-10-18T04:00:00Z"`} { if !strings.Contains(string(raw), want) { t.Errorf("the assignment's value lacks %s: %s", want, raw) } } if a.Key() != "anchor.notes" { t.Errorf("key %s", a.Key()) } // An assignment of today carries none of the new fields, and says none: a reader of today reads the bytes it did. raw, _ = json.Marshal(Assignment{Node: "anchor", Module: "notes", Assigned: at(3)}) if string(raw) != `{"node":"anchor","module":"notes","assigned":"2026-10-18T03:00:00Z"}` { t.Errorf("an assignment without a chain is %s", raw) } // A package in place of an artifact. raw, _ = json.Marshal(Assignment{Node: "a", Module: "m", Package: &Package{Name: "n", Version: "1"}}) if !strings.Contains(string(raw), `"package":{"name":"n","version":"1"}`) { t.Errorf("the package is %s", raw) } } // **The writer without the lease writes nothing** (ADR 0408 §2): it reads, and every create and update is refused with // the bucket untouched. func TestTheWriterWithoutTheLeaseWritesNothing(t *testing.T) { ctx := context.Background() var away error builds, rollouts, assignments := NewMemory(), NewMemory(), NewMemory() c := New(builds, rollouts, assignments, func(context.Context) error { return away }) rev, err := c.Builds.Create(ctx, "b1", Build{ID: "b1"}) if err != nil { t.Fatal(err) } away = errors.New("another controller holds the lease") if _, err := c.Builds.Create(ctx, "b2", Build{ID: "b2"}); !errors.Is(err, ErrNotHeld) { t.Fatalf("a create without the lease answered %v", err) } if _, err := c.Builds.Update(ctx, "b1", Build{ID: "b1", Failed: "x"}, rev); !errors.Is(err, ErrNotHeld) { t.Fatalf("an update without the lease answered %v", err) } if _, err := c.Rollouts.Create(ctx, "r", Rollout{ID: "r"}); !errors.Is(err, ErrNotHeld) { t.Fatalf("a rollout written without the lease: %v", err) } if _, err := c.Assignments.Create(ctx, "a.b", Assignment{}); !errors.Is(err, ErrNotHeld) { t.Fatalf("an assignment written without the lease: %v", err) } if builds.Writes() != 1 || rollouts.Writes() != 0 || assignments.Writes() != 0 { t.Fatalf("writes %d %d %d", builds.Writes(), rollouts.Writes(), assignments.Writes()) } // Reads still answer. if _, ok, err := c.Builds.Get(ctx, "b1"); err != nil || !ok { t.Fatalf("a read without the lease: %v %v", ok, err) } if all, err := c.Builds.All(ctx); err != nil || len(all) != 1 { t.Fatalf("all: %v %v", all, err) } // And a controller with no lease function at all holds nothing. if _, err := New(NewMemory(), NewMemory(), NewMemory(), nil).Builds.Create(ctx, "b", Build{}); !errors.Is(err, ErrNotHeld) { t.Fatalf("a chain with no lease wrote: %v", err) } } // **A bucket that is away is an error, never an empty bucket** (ADR 0227 rule 4). func TestABucketThatIsAwayIsAnErrorNotAnEmptyOne(t *testing.T) { ctx := context.Background() m := NewMemory() c := New(m, NewMemory(), NewMemory(), func(context.Context) error { return nil }) if _, err := c.Builds.Create(ctx, "b1", Build{ID: "b1"}); err != nil { t.Fatal(err) } away := errors.New("the bus is away") m.SetFail(away) if all, err := c.Builds.All(ctx); !errors.Is(err, away) || all != nil { t.Fatalf("all answered %v %v", all, err) } if _, _, err := c.Builds.Get(ctx, "b1"); !errors.Is(err, away) { t.Fatalf("get answered %v", err) } } // A watch hands over every key's value, says it has, and then each write. func TestAWatchHandsOverTheValuesThenEachWrite(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() c := InMemory(nil) if _, err := c.Builds.Create(ctx, "b1", Build{ID: "b1"}); err != nil { t.Fatal(err) } w, err := c.Builds.Watch(ctx) if err != nil { t.Fatal(err) } next := func() Entry { select { case e := <-w: return e case <-ctx.Done(): t.Fatal("the watch said nothing") return Entry{} } } if e := next(); e.Key != "b1" || e.Caught { t.Fatalf("first %+v", e) } if e := next(); !e.Caught { t.Fatalf("second %+v", e) } if _, err := c.Builds.Create(ctx, "b2", Build{ID: "b2"}); err != nil { t.Fatal(err) } if e := next(); e.Key != "b2" { t.Fatalf("third %+v", e) } } // **A key the bus does not take is refused, never rewritten into another** (ADR 0408 §2): a write names the key loudly. func TestAKeyTheBusTakesNoSpellingOfIsRefused(t *testing.T) { ctx := context.Background() c := InMemory(nil) for _, key := range []string{"", "build one", "build*", ".hidden", "trailing.", "a>b"} { if _, err := c.Builds.Create(ctx, key, Build{ID: key}); !errors.Is(err, ErrBadKey) { t.Errorf("the key %q answered %v", key, err) } } for _, key := range []string{"build-1700000000000000000", "anchor.notes", "a/b_c=d"} { if _, err := c.Builds.Create(ctx, key, Build{ID: key}); err != nil { t.Errorf("the key %q answered %v", key, err) } } } // An empty bucket is read as empty, and the read ends. func TestAnEmptyBucketIsReadAsEmpty(t *testing.T) { all, err := InMemory(nil).Assignments.All(context.Background()) if err != nil || len(all) != 0 { t.Fatalf("%v %v", all, err) } }