Files
mesh-delivery/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go
T
jschoubben 39db267630 D1 (builder): the builder names its own module and buckets from its credential's module
Its states' buckets, its finding observer, its build log entries and its step asks' asker take the credential's module (tools: the module the runtime serves them as) instead of constants of its name; builds.StateBucket/BuildsBucket replace the Bucket constants. The readers of the builds bucket (checker, view, grants) still name the builder: the next D1 subtask finds it by the seat mesh-builds.

Builds: design 47 G A-296, §428

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 22:34:08 +01:00

589 lines
30 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"maps"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
sweep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshartifactsweep"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
)
// 1945 (a part exits at start for a bucket it works without): a refused bucket it cannot work without is an error naming the bucket and
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.PlacedNodes,
// issue 774).
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding)
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules")}
if _, err := process.EssentialBucket(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, "builder", "mesh-delivery-indexer_modules", "work"); err == nil ||
!strings.Contains(err.Error(), "grant of mesh-delivery-indexer_modules") {
t.Errorf("a refused bucket the part needs: %v, want an error naming its grant", err)
}
t.Run("ADR 0624: the counted nodes' engine reads", theCountedNodesEngineReads)
}
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the assignments names; a node away
// and never asked has no statement and reads as the baseline's first forms, with no node record read in its place; an
// away node keeps its last statement; a node the assignments no longer names leaves the count, its key removed; and
// builder_engine_reads drops one node's key.
func theCountedNodesEngineReads(t *testing.T) {
gate := buildrun.NewPlacedNodes(t.Logf)
place := func(nodes ...string) {
for _, node := range []string{"n1", "n2"} {
pl := meshmoduleassigner.Assignments{Node: node}
if slices.Contains(nodes, node) {
pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}}
}
values, err := meshmoduleassignerclient.AssignmentsValues(pl)
if err != nil {
t.Fatal(err)
}
for key, raw := range values {
gate.Assignments(key, raw, false, time.Now())
}
if !slices.Contains(nodes, node) {
key, _ := meshmoduleassignerclient.ModuleKey(node, "web")
gate.Assignments(key, nil, true, time.Now())
}
}
}
away := map[string]bool{"n2": true}
asked := map[string]int{}
r := newEngineReads(func(node, key string, _ any) (json.RawMessage, error) {
asked[node]++
if away[node] {
return nil, nats.ErrNoResponders
}
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
}, gate, t.Logf)
r.keeper.Sync = true
kept := map[string][]byte{}
r.keeper.Put = func(node string, v []byte) error { kept[node] = v; return nil }
r.keeper.Delete = func(node string) error { delete(kept, node); return nil }
place("n1", "n2")
r.placed()
present, counted := r.forms()
if !slices.Equal(present["n1"], []string{"form:new"}) || len(present) != 1 || len(counted) != 2 || counted["n2"] != nil {
t.Errorf("away and never asked: present %v, counted %v", present, counted)
}
if s := r.keeper.Statements()["n2"]; s.From != enginereads.FromNone || s.Answered || asked["n2"] != 1 {
t.Errorf("n2, away and never asked, is %+v (asked %d)", s, asked["n2"])
}
// n2 back, stating {word:module}, then away again: it keeps that statement.
away["n2"] = false
r.keeper.Heard("n2", mustStatement(t, enginereads.Statement{Node: "n2", From: enginereads.FromVerb, Answered: true,
AskAgain: &enginereads.Again{By: "n1.ada", Why: "back"}}), false)
away["n2"] = true
r.keeper.AskNode("n2", "its event")
if _, counted = r.forms(); !slices.Equal(counted["n2"], []string{"form:new"}) || r.keeper.Statements()["n2"].Answered {
t.Errorf("away again, n2 reads %v (%+v)", counted["n2"], r.keeper.Statements()["n2"])
}
// n2 retired: it leaves the assignments, and its key goes.
place("n1")
r.placed()
if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil {
t.Errorf("retired n2: counted %v, its key %s", counted, kept["n2"])
}
// builder_engine_reads drops one key.
st := &memReads{v: map[string][]byte{"n1": kept["n1"]}}
tool := enginereads.Tool("builder_engine_reads", "the builder", st, true, time.Now)
if _, err := tool.Run(map[string]any{"node": "n1", "drop": "true", "why": "a test"}); err != nil || st.v["n1"] != nil {
t.Errorf("drop: %v, the key %s", err, st.v["n1"])
}
t.Run("ADR 0668 §3: a manifest-reads call unanswered past five minutes, its machine heard, is call-unanswered", func(t *testing.T) {
// n2 answers nothing: a bound is kept at five minutes and the watchdogs seat's presence is asked only when it ends
// (ADR 0596 §2), once; away, no finding; heard, the finding call-unanswered in the builder's findings at the
// builder's clock; answered, it is cleared.
place("n1", "n2")
now := time.Unix(1000, 0)
clock := func() time.Time { return now }
down := true
r2 := newEngineReads(func(node, _ string, _ any) (json.RawMessage, error) {
if down {
return nil, nats.ErrNoResponders
}
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: now.Format(time.RFC3339Nano)})
}, gate, t.Logf)
r2.keeper.Sync, r2.keeper.Now = true, clock
st := &memFindings{vals: map[string][]byte{}}
armed := map[string]time.Time{}
online, asked := false, []any{}
heard := heardBy(func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key, body)
return json.Marshal(meshwatchdogs.PresenceAnswer{Machine: "n2", Online: online})
})
calls := r2.callsOf(&findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: clock}, func() jetstream.KeyValue { return nil }, heard, t.Logf)
calls.Arm = func(_ context.Context, node string, ends time.Time) error { armed[node] = ends; return nil }
key := meshconditionsclient.Key(meshconditionsclient.ScopeCall, "manifest-reads.n2", meshconditionsclient.KindCallUnanswered)
r2.placed() // n2 asked, unanswered at 1000
if len(st.vals) != 0 || len(asked) != 0 || !armed["n2"].Equal(time.Unix(1000, 0).Add(meshconditionsclient.CallUnansweredAfter)) {
t.Fatalf("before five minutes: findings %v, presence asked %v, bounds %v", st.vals, asked, armed)
}
now = now.Add(meshconditionsclient.CallUnansweredAfter)
r2.expiredCall("n2")
if len(st.vals) != 0 || len(asked) != 2 || asked[0] != presenceInvoke || asked[1] != (meshwatchdogs.PresenceInput{Machine: "n2"}) {
t.Fatalf("its machine away: findings %v, presence asked %v", st.vals, asked)
}
online = true
r2.expiredCall("n2")
f, err := decodeFinding(st.vals[key])
if err != nil || f.Observer != builds.BuilderModule || f.Kind != meshconditionsclient.KindCallUnanswered || len(asked) != 4 {
t.Fatalf("heard five minutes on: finding %+v, %v (keys %v), presence asked %v", f, err, slices.Collect(maps.Keys(st.vals)), asked)
}
r2.stepCalls()
if len(st.vals) != 1 || len(asked) != 4 {
t.Errorf("a step after it: findings %v, presence asked %v", st.vals, asked)
}
down = false
r2.keeper.AskNode("n2", "answered now, its manifest-reads-changed heard")
if len(st.vals) != 0 {
t.Errorf("answered leaves %v", st.vals)
}
})
}
func mustStatement(t *testing.T, s enginereads.Statement) []byte {
t.Helper()
raw, err := enginereads.Encode(s)
if err != nil {
t.Fatal(err)
}
return raw
}
// memReads is the builder's state of engine reads in memory, as the tool reads it.
type memReads struct{ v map[string][]byte }
func (m *memReads) Keys() ([]string, error) { return slices.Collect(maps.Keys(m.v)), nil }
func (m *memReads) Get(key string) ([]byte, uint64, bool, error) {
v, ok := m.v[key]
return v, 1, ok, nil
}
func (m *memReads) Put(key string, value []byte) (uint64, error) { m.v[key] = value; return 2, nil }
func (m *memReads) Delete(key string) error { delete(m.v, key); return nil }
func (m *memReads) Await(string, uint64, func([]byte) bool, time.Duration) ([]byte, error) {
return nil, errors.New("no part answers here")
}
// refusingBus is a bus that refuses: every open (refuseOpen), or every watch of a bucket it opens, with err.
type refusingBus struct {
refuseOpen bool
err error
}
func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) {
if r.refuseOpen {
return nil, r.err
}
return refusedWatch{err: r.err}, nil
}
type refusedWatch struct {
jetstream.KeyValue
err error
}
func (r refusedWatch) WatchAll(context.Context, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
return nil, r.err
}
func (r refusedWatch) Watch(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
return nil, r.err
}
// wakeMsg is a message of the builder's events consumer: its subject and body, acknowledged.
type wakeMsg struct {
jetstream.Msg
subject string
data []byte
acked bool
naked bool
delayed time.Duration
}
func (m *wakeMsg) Subject() string { return m.subject }
func (m *wakeMsg) Data() []byte { return m.data }
func (m *wakeMsg) Ack() error { m.acked = true; return nil }
func (m *wakeMsg) Nak() error { m.naked = true; return nil }
func (m *wakeMsg) NakWithDelay(d time.Duration) error {
m.delayed = d
return nil
}
func (m *wakeMsg) Metadata() (*jetstream.MsgMetadata, error) {
return nil, errors.New("no metadata")
}
func (m *wakeMsg) Headers() nats.Header {
return nil
}
func aFloorsWakeIsHeard(t *testing.T) {
var logs bytes.Buffer
holders := map[string]string{"git": "mesh-delivery-forge-bridge", "mesh-catalog": "mesh-delivery-indexer"}
waits := &versions.FloorWaits{Holder: func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }}
waits.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceRequirements}}, nil, time.Unix(0, 0))
waits.Wait("m/img", versions.SeatImages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
f := &floorWake{b: &build.Builder{}, waits: waits, logf: log.New(&logs, "", 0).Printf}
accepted := &wakeMsg{subject: manifestAcceptedSubject}
if !f.heard(context.Background(), accepted) || !accepted.acked || !strings.Contains(logs.String(), "the floor of m/lib is asked again: manifest-accepted") {
t.Fatalf("manifest-accepted: acked %v, log %q", accepted.acked, logs.String())
}
logs.Reset()
cleared := &wakeMsg{subject: conditionClearedSubject, data: []byte(`{"subject":{"scope":"probe","id":"ace/mesh-delivery-forge-bridge"}}`)}
if !f.heard(context.Background(), cleared) || !strings.Contains(logs.String(), "the floor of m/img is asked again: condition-cleared") {
t.Fatalf("condition-cleared: log %q", logs.String())
}
if f.heard(context.Background(), &wakeMsg{subject: sweep.SubjectArtifactsGone}) {
t.Fatal("artifacts-gone was taken as a floor's wake")
}
// DLV-B4: a registry seat's outcome on the same consumer moves a version; a published wakes a name whose seat was
// silent; one the state cannot take is left for the bus to give again.
logs.Reset()
waits.Wait("m/pkg", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceSeat}}, nil, time.Unix(0, 0))
h := &seatHearer{n: &versions.Numberer{Versions: versions.Versions{Store: noVersions{}}, Waits: waits}, b: &build.Builder{}, logf: log.New(&logs, "", 0).Printf}
byDigest := &wakeMsg{subject: "mesh.seat.go-package-registry.event.published", data: []byte(`{"name":"m/other","digest":"sha256:aa","kind":"package","holder":"gitea"}`)}
if !h.heard(context.Background(), byDigest) || !byDigest.acked || !strings.Contains(logs.String(), "the floor of m/pkg is asked again: go-package-registry said published") {
t.Fatalf("published: acked %v, log %q", byDigest.acked, logs.String())
}
refused := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.refused", data: []byte(`{"name":"m/program","version":"0.1.3","digest":"sha256:bb","reason":"bytes differ"}`)}
if !h.heard(context.Background(), refused) || refused.acked || !refused.naked {
t.Fatalf("a refusal the state cannot take: acked %v, naked %v, log %q", refused.acked, refused.naked, logs.String())
}
if h.heard(context.Background(), &wakeMsg{subject: manifestAcceptedSubject}) {
t.Fatal("manifest-accepted was taken as a seat's outcome")
}
}
// noVersions is a state versions that holds nothing.
type noVersions struct{}
func (noVersions) Get(context.Context, string) ([]byte, uint64, bool, error) {
return nil, 0, false, nil
}
func (noVersions) Create(context.Context, string, []byte) (uint64, error) {
return 0, errors.New("read only")
}
func (noVersions) Update(context.Context, string, []byte, uint64) (uint64, error) {
return 0, errors.New("read only")
}
func (noVersions) KeysOf(context.Context, string) ([]string, error) { return nil, nil }
// failingVersions is a state versions that cannot be read.
type failingVersions struct{ noVersions }
func (failingVersions) KeysOf(context.Context, string) ([]string, error) {
return nil, errors.New("down")
}
func aFailedTrunkBuildIsAFinding(t *testing.T) {
st := &memFindings{vals: map[string][]byte{}}
f := &findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
ended := func(outcome, ref string) builds.BuildLogEntry {
return builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome}
}
key := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m", BuildFailed)
f.buildEnded(context.Background(), ended(builds.BuildFailed, "refs/pull/3/head"))
if len(st.vals) != 0 {
t.Fatalf("a candidate's failed build is a finding: %v", st.vals)
}
f.buildEnded(context.Background(), ended(builds.BuildFailed, ""))
got, err := decodeFinding(st.vals[key])
if err != nil || got.Observer != builds.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") {
t.Fatalf("the finding = %+v, %v", got, err)
}
f.buildEnded(context.Background(), ended(builds.BuildDone, ""))
if _, ok := st.vals[key]; ok {
t.Error("a later done trunk build leaves build-failed")
}
// The floor finding: raised while a name waits on its floor, cleared once it is known.
fw := &versions.FloorWaits{Said: f.floorSaid(context.Background())}
fw.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
floorKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/lib", versions.FloorSilent)
if got, err := decodeFinding(st.vals[floorKey]); err != nil || !strings.Contains(got.Summary, "git.tags did not answer") {
t.Fatalf("the floor finding = %+v, %v", got, err)
}
fw.Answered("m/lib")
if _, ok := st.vals[floorKey]; ok {
t.Error("a known floor leaves floor-silent")
}
// Design 51 *Health and conditions* (review round 1): a holder's refused is publish-refused with its reason, a version
// no holder answered within its bound publish-unanswered, a seat's outcome that does not read seat-outcome-unread;
// a later published of the name clears the first two, the seat's next outcome that reads the third.
vs := versions.Versions{Store: memVersions{}}
for _, v := range []string{"0.1.3", "0.1.4"} {
raw, err := versions.Encode(versions.Entry{Name: "m/program", Kind: versions.KindBundle, Series: "0.1", Version: v, Seat: "oci-artifact-registry", State: versions.Numbered})
if err != nil {
t.Fatal(err)
}
vs.Store.(memVersions)[versions.Key("m/program", v)] = raw
}
pubs := &publishes{bounds: func() jetstream.KeyValue { return nil }, versions: vs, findings: f, bound: versions.PublishBound, logf: t.Logf}
h := &seatHearer{n: &versions.Numberer{Versions: vs}, b: &build.Builder{}, logf: t.Logf, pub: pubs}
refusedKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/program", versions.PublishRefused)
refused := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.refused", data: []byte(`{"name":"m/program","version":"0.1.3","digest":"sha256:bb","reason":"bytes differ"}`)}
if !h.heard(context.Background(), refused) || !refused.acked {
t.Fatalf("a refusal: acked %v", refused.acked)
}
if got, err := decodeFinding(st.vals[refusedKey]); err != nil || !strings.Contains(got.Summary, "refused to publish m/program@0.1.3") || !strings.Contains(got.Summary, "bytes differ") {
t.Fatalf("a holder's refused: the finding = %+v, %v", got, err)
}
pubs.ended(context.Background(), purged{key: versions.PublishBoundKey("m/program", "0.1.4")})
unansweredKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/program", versions.PublishUnanswered)
if got, err := decodeFinding(st.vals[unansweredKey]); err != nil || !strings.Contains(got.Summary, "neither published nor refused of m/program@0.1.4") {
t.Fatalf("a bound ended unanswered: the finding = %+v, %v", got, err)
}
// Review round 2: a bound that cannot be written is publish-unanswered at once; a watch of bounds that cannot start
// is family-not-heard naming the state.
delete(st.vals, unansweredKey)
pubs.arm(context.Background(), versions.Entry{Name: "m/program", Version: "0.1.4", Seat: "oci-artifact-registry"})
if got, err := decodeFinding(st.vals[unansweredKey]); err != nil || !strings.Contains(got.Summary, "its bound could not be written") {
t.Fatalf("a bound not written: the finding = %+v, %v", got, err)
}
watchBounds(context.Background(), refusedWatch{err: errors.New("permissions violation")}, func(jetstream.KeyValueEntry) {}, f.notHeard(context.Background()), nil, t.Logf)
if got, err := decodeFinding(st.vals[heard.Key(builds.BuilderModule, versions.BoundsState)]); err != nil || !strings.Contains(got.Summary, "permissions violation") {
t.Fatalf("bounds not watched: the finding = %+v, %v", got, err)
}
delete(st.vals, heard.Key(builds.BuilderModule, versions.BoundsState))
malformed := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.published", data: []byte(`{"version":"0.1.4"}`)}
unreadKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "oci-artifact-registry", versions.SeatOutcomeUnread)
if !h.heard(context.Background(), malformed) || !malformed.acked || st.vals[unreadKey] == nil {
t.Fatalf("an outcome that does not read: acked %v, findings %v", malformed.acked, st.vals)
}
published := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.published", data: []byte(`{"name":"m/program","version":"0.1.4","digest":"sha256:cc"}`)}
if !h.heard(context.Background(), published) || st.vals[refusedKey] != nil || st.vals[unansweredKey] != nil || st.vals[unreadKey] != nil {
t.Fatalf("a later published leaves the findings: %v", st.vals)
}
// Issue 1966 (ADR 0668 §3, §5; 1748 note 27): a key a look left building because its end could not be written arms its
// bound; the bound's end while the key is still building and no look holds it is call-unanswered about the key; the key's
// next written end deletes the bound and clears it; a key a look holds, or one no longer building, raises nothing; a
// bound that cannot be written raises at once.
buildKey := "m.1111111111111111111111111111111111111111111111111111111111111111"
building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Step: "claimed"})
done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, By: "novox.builder", Step: "built"})
keys := memBuilds{memVersions{buildKey: building}}
looking := false
unputMarks := &memBounds{keys: map[string]bool{}}
u := &unputs{bounds: func() jetstream.KeyValue { return unputMarks }, store: keys, looking: func(string) bool { return looking },
findings: f, bound: meshconditionsclient.CallUnansweredAfter, now: func() time.Time { return time.Unix(600, 0) }, logf: t.Logf}
callKey := meshconditionsclient.Key(meshconditionsclient.ScopeCall, buildKey, meshconditionsclient.KindCallUnanswered)
u.left(context.Background(), buildKey, "nats: timeout")
if !unputMarks.keys[versions.UnputBoundKey(buildKey)] || st.vals[callKey] != nil {
t.Fatalf("a key left building: bound %v, finding %v; want the bound armed, no finding yet", unputMarks.keys, st.vals[callKey] != nil)
}
for name, c := range map[string]struct {
value []byte
look bool
}{"a look holds it": {building, true}, "no longer building": {done, false}} {
keys.memVersions[buildKey], looking = c.value, c.look
u.expired(context.Background(), purged{key: versions.UnputBoundKey(buildKey)})
if st.vals[callKey] != nil {
t.Errorf("%s: the bound's end raised call-unanswered", name)
}
}
keys.memVersions[buildKey], looking = building, false
u.expired(context.Background(), purged{key: versions.UnputBoundKey(buildKey)})
if got, err := decodeFinding(st.vals[callKey]); err != nil || got.Kind != meshconditionsclient.KindCallUnanswered || !strings.Contains(got.Summary, buildKey) || !strings.Contains(got.Needs, build.BuildAgainTool) {
t.Fatalf("the bound's end of a key left building: the finding = %+v, %v", got, err)
}
u.ended(context.Background(), buildKey)
if unputMarks.keys[versions.UnputBoundKey(buildKey)] || st.vals[callKey] != nil {
t.Errorf("the key's end written: bound %v, finding left %v", unputMarks.keys, st.vals[callKey] != nil)
}
u.bounds = func() jetstream.KeyValue { return nil }
u.left(context.Background(), buildKey, "nats: timeout")
if got, err := decodeFinding(st.vals[callKey]); err != nil || !strings.Contains(got.Explanation, "nor its bound") {
t.Fatalf("a bound not written: the finding = %+v, %v", got, err)
}
delete(st.vals, callKey)
// ADR 0615 §5 (review round 1): a consumer not there yet is family-not-heard naming its stream, cleared when it binds.
notHeard := f.notHeard(context.Background())
unheardKey := heard.Key(builds.BuilderModule, "EVENTS")
_ = notHeard.Bind("EVENTS", "a branch's end is not heard", func() error { return errors.New("consumer not found") })
if got, err := decodeFinding(st.vals[unheardKey]); err != nil || got.Kind != heard.Kind || !strings.Contains(got.Summary, "does not hear EVENTS") {
t.Fatalf("a consumer not there: the finding = %+v, %v", got, err)
}
_ = notHeard.Bind("EVENTS", "a branch's end is not heard", func() error { return nil })
if _, ok := st.vals[unheardKey]; ok {
t.Error("a consumer bound leaves family-not-heard")
}
// DLV-B9, the old form's window: one counted node reading only the first form holds it; seven days on it is
// old-form-held about that node; the node stating the newest form closes it, said once, the finding cleared; a
// retired node dropped by hand leaves the count.
now := time.Unix(0, 0)
fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}}
var closings []versions.OldFormClosedBody
var logged []builds.BuildLogEntry
marks := &memBounds{keys: map[string]bool{}}
window := &oldForm{module: builds.BuilderModule, forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now }, bounds: func() jetstream.KeyValue { return marks },
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) },
log: func(e builds.BuildLogEntry) { logged = append(logged, e) }}
reads := &engineReads{module: builds.BuilderModule, keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
// Issue 1966 note 155, ADR 0668 §4: at the consumer's bind every counted node is called once again.
var boundCalls []string
reads.keeper.Sync = true
reads.keeper.Call = func(node, _ string, _ any) (json.RawMessage, error) {
boundCalls = append(boundCalls, node)
return nil, errors.New("not served")
}
reads.atBind(now)
if !slices.Equal(boundCalls, []string{"a", "b"}) {
t.Fatalf("at the bind called %v, want every counted node once", boundCalls)
}
reads.keeper = &enginereads.Keeper{}
newest := []string{forms.FormNeed("oci")}
counted := map[string][]string{"a": newest, "b": nil}
window.step(context.Background(), []string{"a", "b"}, counted)
heldKey := meshconditionsclient.Key(meshconditionsclient.ScopeMachine, "b", versions.OldFormHeld)
if !window.w.Open || !slices.Equal(window.w.Holding, []string{"b"}) || st.vals[heldKey] != nil {
t.Fatalf("one node lacking the newest form: %+v", window.w)
}
// Review round 1: the node holding it has a bound in the state bounds ending at its seven days, so old-form-held is
// raised then, not at the next change.
if !marks.keys[versions.OldFormBoundKey("b")] || marks.keys[versions.OldFormBoundKey("a")] {
t.Fatalf("the old form's bounds = %v, want b's alone", marks.keys)
}
// Security-upgrade F3 (review round 1): an empty node_haves read, naming no node, leaves the window open.
window.step(context.Background(), nil, map[string][]string{})
if !window.w.Open || !slices.Equal(window.w.Holding, []string{"b"}) || len(closings) != 0 {
t.Fatalf("an empty read closed the window: %+v, closings %+v", window.w, closings)
}
now = now.Add(versions.FindingAfter)
window.step(context.Background(), []string{"a", "b"}, counted)
if got, err := decodeFinding(st.vals[heldKey]); err != nil || !strings.Contains(got.Summary, "the node-engine of b") {
t.Fatalf("seven days held: the finding = %+v, %v", got, err)
}
counted["b"] = newest
window.step(context.Background(), []string{"a", "b"}, counted)
window.step(context.Background(), []string{"a", "b"}, counted)
if marks.keys[versions.OldFormBoundKey("b")] {
t.Error("a node no longer holding the window keeps its bound")
}
if window.w.Open || len(closings) != 1 || !slices.Equal(closings[0].LastHolding, []string{"b"}) || st.vals[heldKey] != nil {
t.Errorf("the node back with the newest form: %+v, closings %+v, finding left %v", window.w, closings, st.vals[heldKey] != nil)
}
if len(logged) != 1 || logged[0].Event != builds.BuildSaidEvent || logged[0].Why != versions.SaidOldFormClosed ||
!strings.Contains(logged[0].Line, "last held by b") {
t.Errorf("the build log = %+v, want the close said once (build.said)", logged)
} else if _, err := builds.EncodeBuildLogEntry(logged[0]); err != nil {
t.Errorf("the close is no entry the build log takes: %v", err)
}
if _, err := window.drop(context.Background(), "b", "op@shanks", "retired", now); err != nil {
t.Fatal(err)
}
if _, c := reads.forms(); len(c) != 1 || c["b"] != nil {
t.Errorf("a node dropped by hand is still counted: %v", c)
}
}
// memVersions is a state versions in memory.
type memVersions map[string][]byte
func (m memVersions) Get(_ context.Context, key string) ([]byte, uint64, bool, error) {
v, ok := m[key]
return v, 1, ok, nil
}
func (m memVersions) Create(_ context.Context, key string, v []byte) (uint64, error) {
m[key] = v
return 1, nil
}
func (m memVersions) Update(_ context.Context, key string, v []byte, _ uint64) (uint64, error) {
m[key] = v
return 2, nil
}
func (m memVersions) KeysOf(context.Context, string) ([]string, error) {
return slices.Collect(maps.Keys(m)), nil
}
// memBuilds is the builder's builds in memory.
type memBuilds struct{ memVersions }
func (m memBuilds) Keys(context.Context) ([]string, error) {
return slices.Collect(maps.Keys(m.memVersions)), nil
}
// memBounds is the state bounds in memory: which keys stand.
type memBounds struct {
jetstream.KeyValue
keys map[string]bool
}
func (m *memBounds) Create(_ context.Context, key string, _ []byte, _ ...jetstream.KVCreateOpt) (uint64, error) {
if m.keys[key] {
return 0, jetstream.ErrKeyExists
}
m.keys[key] = true
return 1, nil
}
func (m *memBounds) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
delete(m.keys, key)
return nil
}
// purged is the bus's marker of a key whose time to live ended.
type purged struct {
jetstream.KeyValueEntry
key string
}
func (p purged) Key() string { return p.key }
func (p purged) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePurge }
// memFindings is a findings bucket in memory.
type memFindings struct{ vals map[string][]byte }
// Write keeps the finding under its key, as the held writer does in the state.
func (m *memFindings) Write(_ context.Context, f mcseat.Finding) error {
v, err := json.Marshal(f)
m.vals[views.FindingKey(f.Scope, f.ID, f.Kind)] = v
return err
}
// Clear deletes the finding of scope, id and kind.
func (m *memFindings) Clear(_ context.Context, scope, id, kind, _ string) error {
delete(m.vals, views.FindingKey(scope, id, kind))
return nil
}
// decodeFinding reads a finding as the writer kept it.
func decodeFinding(v []byte) (mcseat.Finding, error) {
var f mcseat.Finding
return f, json.Unmarshal(v, &f)
}
// Design 47 G A-296: the builder's findings are observed by the module its credential names, never a constant of its name.
func TestTheBuildersFindingsAreObservedByItsCredentialsModule(t *testing.T) {
st := &memFindings{vals: map[string][]byte{}}
f := &findings{module: "acme-builder", w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
f.buildEnded(context.Background(), builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Outcome: builds.BuildFailed})
got, err := decodeFinding(st.vals[meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m", BuildFailed)])
if err != nil || got.Observer != "acme-builder" {
t.Fatalf("the finding = %+v, %v; want the observer acme-builder", got, err)
}
}