Files
mesh-controller/internal/inventory/builds_test.go
T
jschoubben 0bbb5c6838 Builds have a history, and failures are rows like any other
A build result was answered to whoever asked and kept nowhere. So "when
did this last build", "why did it fail" and "which machine built what is
running" had no answer, and a build nobody was waiting for was reported
into the void — which is the same as not reporting it.

Failures are recorded too, and that is the point rather than a detail: a
failed build that leaves no trace is indistinguishable from one nobody
asked for, and the difference is the whole of whether somebody should be
looking at something. A build that never learned what it was building
keeps the repository, because that is what a person goes and looks at.

Recording is idempotent on the correlation id, because a result can
arrive twice — as the answer to whoever asked, and on the exchange when
nobody was. Two rows would show one build as two, and which is real is
not answerable afterwards.

The serving control plane now binds `built` as well, so results from
builds it did not ask for are kept. It refuses them loudly when it has
nowhere to put them rather than dropping them, so the broker's own
counters show something arriving that nothing handles.

`builds [<module>]` reads it: what happened lately across the mesh, or
what has happened to one module — the first asked after something goes
wrong, the second when deciding whether to trust something.

What was published is kept with the build, so a digest traces back to
what made it without holding the manifest twice in a place that can
disagree with the first.
2026-08-30 10:18:23 +02:00

139 lines
3.8 KiB
Go

package inventory
import (
"context"
"strings"
"testing"
)
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
// "why did it fail" and "which machine built what is running" had no answer at all.
func aBuild(id, module, failed string) Build {
b := Build{
ID: id, Repository: "https://forge.invalid/" + strings.TrimSuffix(module, "?") + ".git",
Module: module, On: "a-build-machine", Failed: failed,
}
if failed == "" {
b.Commit = "c0ffee" + id
b.Made = []Artifact{{Name: "config", Kind: "archive", Reference: "…/blobs/sha256:…"}}
}
return b
}
func TestAFailedBuildIsARowLikeAnyOther(t *testing.T) {
// One that leaves no trace is indistinguishable from a build nobody asked for, and the
// difference is the whole of whether somebody should be looking at something.
inv := fresh(t)
ctx := context.Background()
if err := inv.RecordBuild(ctx, aBuild("1", "", "cannot clone: no such repository")); err != nil {
t.Fatal(err)
}
got, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("got %d builds", len(got))
}
if got[0].Worked() {
t.Fatal("a failure was recorded as a success")
}
if !strings.Contains(got[0].Failed, "no such repository") {
t.Fatalf("the builder's own words were not kept: %q", got[0].Failed)
}
// And it kept what was asked for, which is the only thing a person can go and look at when
// the build never learned what it was building.
if got[0].Module != "" || got[0].Repository == "" {
t.Fatalf("got %+v", got[0])
}
}
func TestOneResultRecordedTwiceIsOneBuild(t *testing.T) {
// A result can arrive twice: as the answer to whoever asked, and on the exchange when nobody
// was. Two rows would show one build as two, and which is real is not answerable afterwards.
inv := fresh(t)
ctx := context.Background()
for i := 0; i < 2; i++ {
if err := inv.RecordBuild(ctx, aBuild("same", "shell", "")); err != nil {
t.Fatal(err)
}
}
got, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("one build was recorded %d times", len(got))
}
}
func TestBuildsComeBackNewestFirstAndCanBeAskedPerModule(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
for _, b := range []Build{
aBuild("1", "shell", ""),
aBuild("2", "meshboard", ""),
aBuild("3", "shell", "the tests failed"),
} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
all, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(all) != 3 || all[0].ID != "3" {
t.Fatalf("newest is not first: %v", ids(all))
}
// Per module, because "what has happened to this" is asked when deciding whether to trust it.
shell, err := inv.Builds(ctx, "shell", 10)
if err != nil {
t.Fatal(err)
}
if len(shell) != 2 {
t.Fatalf("shell has %d builds: %v", len(shell), ids(shell))
}
for _, b := range shell {
if b.Module != "shell" {
t.Fatalf("asked for shell and got %q", b.Module)
}
}
}
func TestWhatWasPublishedIsKeptWithTheBuild(t *testing.T) {
// So a digest can be traced back to the build that made it, without keeping the manifest a
// second time in a place that can disagree with the first.
inv := fresh(t)
ctx := context.Background()
if err := inv.RecordBuild(ctx, aBuild("1", "shell", "")); err != nil {
t.Fatal(err)
}
got, _ := inv.Builds(ctx, "", 10)
if len(got[0].Made) != 1 || got[0].Made[0].Kind != "archive" {
t.Fatalf("what was published was not kept: %+v", got[0].Made)
}
}
func TestAskingForMoreThanThereIsIsNotAnError(t *testing.T) {
inv := fresh(t)
got, err := inv.Builds(context.Background(), "", 100)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("got %d", len(got))
}
}
func ids(builds []Build) []string {
var out []string
for _, b := range builds {
out = append(out, b.ID)
}
return out
}