Files
mesh-controller/cmd/mesh-controller/held_test.go
T
jschoubben 3b51d3f86e
mesh/delivery-group group fix/1937-done-key-blobs rejected: composed together: the checker's verdict at the members' heads (c20fdcb8c623): integration: github.…
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: integration: github.com/novox/mesh-controller/cmd/mesh-controller.TestAWarrantForRetryRetriesAFailedPlanInTheStoreAndEndsItsConditi…
Answer held and say artifacts-gone, so the builder rebuilds a done key whose blob the artifact store no longer holds (issue 1937)
A done key the controller never took in holds no manifest, and the store's nightly collector deleted its bare blob:
node-tools', mesh-host's and the forge bridge's keys name blobs that answer 404. Since issue 1933 the take-in refuses such a
key, and nothing built it again. held (in the serving process, HEADs only) lets the builder ask before it reuses a key; a
refused take-in now names every gone archive and says artifacts-gone with the module, key, fingerprint and references.
2026-10-24 04:12:02 +02:00

133 lines
4.7 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"slices"
"strings"
"sync"
"testing"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-controller/internal/artifacts"
"github.com/novox/mesh-controller/internal/link"
)
// 1937/AC1: held answers each reference held, gone (the store's 404) or unknown, asks only HEADs, and its answer is what
// the builder reads (mesh-sdk delivery.DecodeHeldAnswer).
func TestHeldAnswersEachReferenceHeldOrGone(t *testing.T) {
present := "artifact-store://node-tools/runtime/blobs/" + shaOf("held")
missing := "artifact-store://node-tools/runtime/blobs/" + shaOf("gone")
broken := "artifact-store://mesh-host/host-arch/blobs/" + shaOf("broken")
address, ask := heldAddress, heldAsk
t.Cleanup(func() { heldAddress, heldAsk = address, ask })
heldAddress = func(context.Context) (string, error) { return "store.test:5100", nil }
var asked []string
var mu sync.Mutex
heldAsk = func(_ context.Context, at, reference string) error {
mu.Lock()
asked = append(asked, at+" "+reference)
mu.Unlock()
switch reference {
case missing:
return artifacts.Gone
case broken:
return errors.New("the artifact store answered 500")
}
return nil
}
got, err := heldAnswer(t.Context(), present+", "+missing+","+broken+","+present)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(got)
if err != nil {
t.Fatal(err)
}
a, err := sdkdelivery.DecodeHeldAnswer(raw, []string{present, missing, broken})
if err != nil {
t.Fatalf("the builder cannot read held's answer: %v\n%s", err, raw)
}
if !slices.Equal(a.Held, []string{present}) || !slices.Equal(a.Gone, []string{missing}) || a.Unknown[broken] == "" {
t.Errorf("held answered %+v", a)
}
if len(asked) != 3 {
t.Errorf("held asked the store %d times for three distinct references: %v", len(asked), asked)
}
if !strings.Contains(got.(verbAnswer).Output, missing) {
t.Errorf("held's words do not name the gone reference: %s", got.(verbAnswer).Output)
}
for _, refused := range []string{"", "registry.example/x@sha256:00"} {
if _, err := heldAnswer(t.Context(), refused); err == nil {
t.Errorf("held took %q", refused)
}
}
// A verb answered in the serving process, never by a command line.
if !inProcess["held"] {
t.Error("held is not answered by the serving controller")
}
if _, err := argvFor("held", map[string]any{"references": present}); err == nil {
t.Error("held composed a command line")
}
}
// saidGone keeps the artifacts-gone events said.
type saidGone struct {
link.Bus // only PublishSeatEvent is said on
mu sync.Mutex
said []sdkdelivery.ArtifactsGone
}
func (s *saidGone) PublishSeatEvent(_ context.Context, seat, event string, body []byte) error {
if seat != link.MeshControllerSeat || event != link.KeyArtifactsGone {
return errors.New("said under the wrong seat or name: " + seat + " " + event)
}
e, err := sdkdelivery.DecodeArtifactsGone(body)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
s.said = append(s.said, e)
return nil
}
// 1937/AC2: a take-in refused for a gone archive names every gone one and says artifacts-gone with the module, the key,
// its fingerprint and the gone references, so the builder builds the key again; a hand registration says nothing.
func TestARefusedTakeInSaysArtifactsGone(t *testing.T) {
program := "artifact-store://x/tools/blobs/" + shaOf("t")
image := "artifact-store://x/img@" + shaOf("i")
keys, trunk := aKeyOf(tsBundle, 3,
sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: shaOf("t"), Reference: program},
sdkdelivery.BuiltArtifact{Name: "img", Kind: "image", Digest: shaOf("i"), Reference: image})
bus := &saidGone{}
was := checkEvents
checkEvents = bus
t.Cleanup(func() { checkEvents = was })
gone := func(context.Context, string) error { return artifacts.Gone }
_, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{hold: gone})
if err == nil || !strings.Contains(err.Error(), program) || !strings.Contains(err.Error(), image) ||
!strings.Contains(err.Error(), "not in the artifact store") {
t.Fatalf("a take-in of a key whose archives are gone: %v", err)
}
if len(bus.said) != 1 {
t.Fatalf("said %d artifacts-gone events; want one", len(bus.said))
}
e := bus.said[0]
wantKey, _ := sdkdelivery.BuilderBuildKey("x", keyFP)
if e.Module != "x" || e.Fingerprint != keyFP || e.Key != wantKey || !slices.Equal(e.Gone, []string{image, program}) {
t.Errorf("said %+v", e)
}
bus.said = nil
_, _, _ = buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk, Hand: map[string]string{"tools": program}},
keyContext{hold: gone})
if len(bus.said) != 0 {
t.Errorf("a registration by hand said %v: no builder's key was refused", bus.said)
}
}