Merge branch 'feat/47-G-build' into feat/47-G-build-builder

This commit is contained in:
2026-11-11 11:44:53 +01:00
49 changed files with 1800 additions and 451 deletions
@@ -6,3 +6,12 @@ require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+20 -6
View File
@@ -1,12 +1,26 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
@@ -0,0 +1,200 @@
package meshcatalogclient
import (
"context"
"encoding/json"
"fmt"
"slices"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/views"
)
// RegistrationsVerb is the seat verb a part takes its first view of the registrations by, as its manifest's invokes
// names it.
const RegistrationsVerb = "seat:" + seat.Seat + "." + seat.VerbRegistrations
// Call asks one seat verb over the bus and answers what the holder said, whatever wraps it (process.Conn.Call, or
// stdio.Ask through the tool runner).
type Call func(key string, body any) (json.RawMessage, error)
// registrationsCreated stands for the picture's Created in its follower: the registrations are followed by the seat's
// event, never by an owner's views family, so one fixed time, never zero.
var registrationsCreated = time.Unix(0, 0).UTC()
// Registrations is a part's typed follower of the registrations (design 47 G §353): a first view asked of the seat's verb
// `registrations`, then each `registration-changed` heard, built on go/views' Follower, so the runtime's tools
// `<module>_first_views` and `<module>_first_view_again` read and steer it as every first view. Its picture is keyed by
// module, each value the module's newest registration. Ready is closed at the first view: a part that switches on its
// first registrations waits for it.
type Registrations struct {
Follower *views.Follower
call Call
}
// NewRegistrations is a Registrations asking the seat through call. Nothing is asked before Start.
func NewRegistrations(call Call) *Registrations {
r := &Registrations{call: call}
r.Follower = &views.Follower{Owner: seat.Seat, Tool: RegistrationsVerb, State: seat.StateRegistrations, Ask: r.ask}
return r
}
// WireRegistrations is a Registrations adopted by a stdio part's views.Reader: the reader writes its conditions, reads it
// as `mesh-catalog.registrations`, lists and asks it again in its first-view tools, and at its Start subscribes the
// seat's event registration-changed (the part's manifest consumes it) into Hear and asks the first view. changed, when
// not nil, is told of each module whose registration the picture changes, after the reader's own. The part declares the
// seat's verb registrations in invokes.
func WireRegistrations(r *views.Reader, call Call, changed func(reg seat.Registration, gone bool)) (*Registrations, error) {
g := NewRegistrations(call)
g.Follower.Changed = typedChanged(changed)
if err := r.Adopt(g.Follower, seat.Seat, seat.EventRegistrationChanged, g.Hear); err != nil {
return nil, err
}
return g, nil
}
// WireRegistrationsProcess is WireRegistrations for a process's views.ProcessReader: its picture
// (`mesh-catalog_registrations`) reads the registrations as a read-only bucket, and the seat's event
// registration-changed on the module's durable consumer goes to Hear.
func WireRegistrationsProcess(r *views.ProcessReader, call Call, changed func(reg seat.Registration, gone bool)) (*Registrations, *views.Picture, error) {
g := NewRegistrations(call)
g.Follower.Changed = typedChanged(changed)
p, err := r.Adopt(g.Follower, seat.Seat, seat.EventRegistrationChanged, g.Hear)
if err != nil {
return nil, nil, err
}
return g, p, nil
}
func typedChanged(changed func(seat.Registration, bool)) func(string, json.RawMessage, bool, time.Time) {
if changed == nil {
return nil
}
return func(module string, value json.RawMessage, gone bool, _ time.Time) {
reg := seat.Registration{Module: module}
if !gone {
_ = json.Unmarshal(value, &reg)
}
changed(reg, gone)
}
}
// Start asks the first view until it is answered or ctx ends.
func (r *Registrations) Start(ctx context.Context) error { return r.Follower.Start(ctx) }
// Ready is closed once the first view is answered.
func (r *Registrations) Ready() <-chan struct{} { return r.Follower.Ready() }
// Registration is one module's newest registration in the picture.
func (r *Registrations) Registration(module string) (seat.Registration, bool) {
raw, ok := r.Follower.Get(module)
if !ok {
return seat.Registration{}, false
}
var reg seat.Registration
if err := json.Unmarshal(raw, &reg); err != nil {
return seat.Registration{}, false
}
return reg, true
}
// All is every registration in the picture, by module name.
func (r *Registrations) All() []seat.Registration {
values := r.Follower.Values()
out := make([]seat.Registration, 0, len(values))
for _, raw := range values {
var reg seat.Registration
if json.Unmarshal(raw, &reg) == nil {
out = append(out, reg)
}
}
slices.SortFunc(out, func(a, b seat.Registration) int {
if a.Module < b.Module {
return -1
}
if a.Module > b.Module {
return 1
}
return 0
})
return out
}
// Hear takes one `registration-changed` event, as the part's durable consumer of the seat's events hands it. A version
// the picture holds, or one older, is passed over; a newer one asks the first view again on its own goroutine, so the
// consumer is never held while the holder is away, and again until the picture holds that version or ctx ends (an ask
// already running, the first view among them, may have been answered before the change was written). Data that is no such event is an error.
func (r *Registrations) Hear(ctx context.Context, data []byte) error {
var ev seat.RegistrationChangedEvent
if err := json.Unmarshal(data, &ev); err != nil || ev.Module == "" || ev.Version <= 0 {
return fmt.Errorf("meshcatalogclient: not a %s event: %s", seat.EventRegistrationChanged, clipBytes(data))
}
if r.holds(ev.Module, ev.Version) {
return nil
}
go r.catchUp(ctx, ev.Module, ev.Version)
return nil
}
func (r *Registrations) holds(module string, version int64) bool {
reg, ok := r.Registration(module)
return ok && reg.Version >= version
}
// catchUp asks the first view again until the picture holds version of module, pausing between asks as the follower's
// Wait says (doubling to a minute), or until ctx ends.
func (r *Registrations) catchUp(ctx context.Context, module string, version int64) {
wait := r.Follower.Wait
if wait == 0 {
wait = 5 * time.Second
}
for !r.holds(module, version) {
if err := r.Follower.Gap(ctx, fmt.Sprintf("registration of %s at version %d said", module, version)); err != nil {
return
}
if r.holds(module, version) {
return
}
p, cancel := context.WithTimeout(ctx, wait)
<-p.Done()
cancel()
if ctx.Err() != nil {
return
}
if wait *= 2; wait > time.Minute {
wait = time.Minute
}
}
}
// ask is the Follower's Ask: every registration, whole in one answer.
func (r *Registrations) ask(_ context.Context, _ string) (views.Answer, error) {
raw, err := r.call(RegistrationsVerb, seat.RegistrationsInput{})
if err != nil {
return views.Answer{}, err
}
answer, said, ok := buscalls.Unwrap(raw)
if !ok {
return views.Answer{}, fmt.Errorf("%s answered: %s", RegistrationsVerb, said)
}
var a seat.RegistrationsAnswer
if err := json.Unmarshal(answer, &a); err != nil || a.Registrations == nil {
return views.Answer{}, fmt.Errorf("%s answered no registrations: %s", RegistrationsVerb, clipBytes(answer))
}
values := make(map[string]json.RawMessage, len(a.Registrations))
for _, reg := range a.Registrations {
v, _ := json.Marshal(reg)
values[reg.Module] = v
}
return views.Answer{V: 1, Owner: seat.Seat, State: seat.StateRegistrations, Created: registrationsCreated, Values: values, At: time.Now()}, nil
}
func clipBytes(b []byte) string {
if len(b) > 200 {
return string(b[:200]) + "…"
}
return string(b)
}
@@ -0,0 +1,140 @@
package meshcatalogclient
import (
"context"
"encoding/json"
"fmt"
"sync"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// fakeRegistrations answers the verb registrations from the versions it keeps, and counts each ask.
type fakeRegistrations struct {
mu sync.Mutex
versions map[string]int64
asks int
// stale answers the next ask from these versions, once: an answer read before a change was written.
stale map[string]int64
}
func (f *fakeRegistrations) call(key string, body any) (json.RawMessage, error) {
if key != RegistrationsVerb {
return nil, fmt.Errorf("asked %s", key)
}
if in := body.(seat.RegistrationsInput); in.Module != "" || in.History {
return nil, fmt.Errorf("asked %+v", in)
}
f.mu.Lock()
defer f.mu.Unlock()
f.asks++
versions := f.versions
if f.stale != nil {
versions, f.stale = f.stale, nil
}
a := seat.RegistrationsAnswer{Registrations: []seat.Registration{}}
for m, v := range versions {
a.Registrations = append(a.Registrations, seat.Registration{Module: m, Version: v, Origin: "novox/" + m, Path: "module.json", Branches: []string{"main"}})
}
return json.Marshal(a)
}
func (f *fakeRegistrations) set(module string, version int64) {
f.mu.Lock()
defer f.mu.Unlock()
f.versions[module] = version
}
func (f *fakeRegistrations) asked() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.asks
}
func changedEvent(module string, version int64) []byte {
b, _ := json.Marshal(seat.RegistrationChangedEvent{V: 1, Module: module, Version: version, Moved: []string{"branches"}, Warrant: "ask-1"})
return b
}
// CT10 (design 47 G §353): a part following the registrations takes a first view of the verb registrations, then each
// registration-changed: a newer version is asked and held, one it holds asks nothing, a change heard while an answer
// read before it was given asks again, and what is no such event is refused.
func TestRegistrationsAreAFirstViewThenTheirEvents(t *testing.T) {
changed := make(chan seat.Registration, 16)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
next := func() seat.Registration {
t.Helper()
select {
case reg := <-changed:
return reg
case <-ctx.Done():
t.Fatal("no registration changed")
return seat.Registration{}
}
}
f := &fakeRegistrations{versions: map[string]int64{"nats": 1, "zsh": 3}}
r := NewRegistrations(f.call)
r.Follower.Wait = time.Nanosecond
r.Follower.Changed = typedChanged(func(reg seat.Registration, gone bool) {
if !gone {
changed <- reg
}
})
select {
case <-r.Ready():
t.Fatal("ready before its first view")
default:
}
if err := r.Start(ctx); err != nil {
t.Fatal(err)
}
<-r.Ready()
if reg, ok := r.Registration("nats"); !ok || reg.Version != 1 || reg.Origin != "novox/nats" {
t.Fatalf("first view holds nats as %+v, %v", reg, ok)
}
if all := r.All(); len(all) != 2 || all[0].Module != "nats" || all[1].Module != "zsh" {
t.Fatalf("All = %+v", all)
}
for range 2 {
next()
}
if err := r.Hear(ctx, changedEvent("zsh", 3)); err != nil {
t.Fatal(err)
}
if n := f.asked(); n != 1 {
t.Errorf("a version held asked the first view again: %d asks", n)
}
f.set("nats", 2)
if err := r.Hear(ctx, changedEvent("nats", 2)); err != nil {
t.Fatal(err)
}
if reg := next(); reg.Module != "nats" || reg.Version != 2 {
t.Fatalf("after registration-changed: %+v", reg)
}
// The answer to the ask the event starts was read before the change was written: it is asked once more.
f.mu.Lock()
f.stale = map[string]int64{"nats": 2, "zsh": 3}
f.mu.Unlock()
f.set("zsh", 4)
if err := r.Hear(ctx, changedEvent("zsh", 4)); err != nil {
t.Fatal(err)
}
if reg := next(); reg.Module != "zsh" || reg.Version != 4 {
t.Fatalf("after a stale answer: %+v", reg)
}
if n := f.asked(); n != 4 {
t.Errorf("asks = %d, want 4 (first view, nats, a stale answer and the one after)", n)
}
for _, data := range []string{`{}`, `not json`, `{"module":"nats"}`} {
if err := r.Hear(ctx, []byte(data)); err == nil {
t.Errorf("Hear(%s) took it", data)
}
}
}
@@ -418,8 +418,10 @@ type Pipeline struct {
// the watched branch, or the branch of a run on request
Branch string `json:"branch"`
// the commit the module's entry was last changed at
Commit string `json:"commit"`
Ended *time.Time `json:"ended,omitempty"`
Commit string `json:"commit"`
// how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)
EndCount int64 `json:"end-count,omitempty"`
Ended *time.Time `json:"ended,omitempty"`
// the module's build-input fingerprint at the commit
Fp string `json:"fp,omitempty"`
// the gathered group, when the pipeline is its
@@ -463,8 +465,10 @@ type PipelineEnded struct {
// the watched branch, or the branch of a run on request
Branch string `json:"branch"`
// the commit the module's entry was last changed at
Commit string `json:"commit"`
Ended *time.Time `json:"ended,omitempty"`
Commit string `json:"commit"`
// how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)
EndCount int64 `json:"end-count,omitempty"`
Ended *time.Time `json:"ended,omitempty"`
// the module's build-input fingerprint at the commit
Fp string `json:"fp,omitempty"`
// the gathered group, when the pipeline is its
@@ -630,6 +634,7 @@ const (
StepResultOutcomeWaitingForABuildNode = "waiting for a build node"
StepResultOutcomeDidNotBuild = "did not build"
StepResultOutcomeAccepted = "accepted"
StepResultOutcomeNotAsked = "not asked"
StepResultOutcomeDeclined = "declined"
StepResultOutcomeSuperseded = "superseded"
StepResultOutcomeCancelled = "cancelled"
@@ -19,9 +19,9 @@ func TestFixtures(t *testing.T) {
}{
{name: "a pipeline started with the steps its policy gives", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-started.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"at\":\"2026-11-10T10:00:00Z\",\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"status\":\"required\",\"step\":\"lint\"},{\"status\":\"bypass\",\"step\":\"review\"}],\"v\":1}", wire: "{\"at\":\"2026-11-10T10:00:00Z\",\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"status\":\"required\",\"step\":\"lint\"},{\"status\":\"bypass\",\"step\":\"review\"}],\"v\":1}",
decode: func(b []byte) (any, error) { var v PipelineStartedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-started\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")},
{name: "a pipeline ended passed, an optional step failed, over budget and widening named", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}",
{name: "a pipeline ended passed, an optional step failed, over budget and widening named", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"tests\":{\"integration\":3,\"unit\":42},\"v\":1,\"widens\":[{\"name\":\"mesh-build-go:program\",\"said\":\"its test runner\",\"what\":\"serving part\"}]}",
decode: func(b []byte) (any, error) { var v PipelineEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")},
{name: "a pipeline ended failed, naming the step that held it", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"on\":\"the operator\",\"outcome\":\"waiting\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"on\":\"the operator\",\"outcome\":\"waiting\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}",
{name: "a pipeline ended failed, naming the step that held it, its approval not asked", subject: "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"outcome\":\"not asked\",\"status\":\"required\",\"step\":\"approval\",\"why\":\"unit of nats:program failed\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"failed\",\"over-budget\":true,\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"outcome\":\"failed\",\"status\":\"required\",\"step\":\"unit\"},{\"outcome\":\"not asked\",\"status\":\"required\",\"step\":\"approval\",\"why\":\"unit of nats:program failed\"}],\"v\":1,\"why\":\"unit of nats:program failed\"}",
decode: func(b []byte) (any, error) { var v PipelineEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.pipeline-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")},
{name: "a group's one pipeline ended on a watched branch", subject: "mesh.seat.mesh-pipeline-runner.event.group-ended.main.registry-1966", given: "{\"branch\":\"main\",\"ended\":\"2026-11-10T10:20:00Z\",\"group\":\"registry-1966\",\"outcome\":\"passed\",\"pipelines\":[{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"group\":\"registry-1966\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"}],\"v\":1}],\"revision\":12,\"source\":3589,\"v\":1}", wire: "{\"branch\":\"main\",\"ended\":\"2026-11-10T10:20:00Z\",\"group\":\"registry-1966\",\"outcome\":\"passed\",\"pipelines\":[{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"group\":\"registry-1966\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"}],\"v\":1}],\"revision\":12,\"source\":3589,\"v\":1}",
decode: func(b []byte) (any, error) { var v GroupEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.group-ended\\.[^.*> ]+\\.[^.*> ]+$")},
@@ -37,7 +37,7 @@ func TestFixtures(t *testing.T) {
decode: func(b []byte) (any, error) { var v WaitHeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.wait-held\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")},
{name: "the wait ended answered", subject: "mesh.seat.mesh-pipeline-runner.event.wait-ended.nats.main.0123456789abcdef0123456789abcdef01234567", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"how\":\"answered\",\"module\":\"nats\",\"on\":\"person\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"since\":\"2026-11-10T10:10:00Z\",\"source\":3589,\"step\":\"approval\",\"v\":1,\"who\":\"the operator\"}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fp\":\"fp-0123\",\"how\":\"answered\",\"module\":\"nats\",\"on\":\"person\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"since\":\"2026-11-10T10:10:00Z\",\"source\":3589,\"step\":\"approval\",\"v\":1,\"who\":\"the operator\"}",
decode: func(b []byte) (any, error) { var v WaitEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-pipeline-runner\\.event\\.wait-ended\\.[^.*> ]+\\.[^.*> ]+\\.[^.*> ]+$")},
{name: "a pipeline kept", subject: "", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}",
{name: "a pipeline kept", subject: "", given: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}", wire: "{\"branch\":\"main\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"end-count\":1,\"ended\":\"2026-11-10T10:20:00Z\",\"fp\":\"fp-0123\",\"module\":\"nats\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"revision\":12,\"source\":3589,\"started\":\"2026-11-10T10:00:00Z\",\"steps\":[{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"fingerprint\":\"fp-0123\",\"node\":\"ace\",\"outcome\":\"passed\",\"seat\":\"node-build-go\",\"status\":\"required\",\"step\":\"lint\"},{\"artifact\":\"nats:program\",\"carried-from\":\"main.11.nats\",\"fingerprint\":\"fp-0123\",\"outcome\":\"carried\",\"status\":\"required\",\"step\":\"unit\",\"tests\":{\"unit\":42}},{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"node\":\"g14\",\"outcome\":\"failed\",\"seat\":\"node-build-go\",\"status\":\"optional\",\"step\":\"integration\",\"tests\":{\"integration\":3},\"why\":\"TestTheBusWhole failed\"},{\"outcome\":\"passed\",\"seat\":\"mesh-wants\",\"status\":\"required\",\"step\":\"composition\"},{\"outcome\":\"bypassed\",\"status\":\"bypass\",\"step\":\"review\"},{\"outcome\":\"passed\",\"status\":\"required\",\"step\":\"approval\"}],\"v\":1}",
decode: func(b []byte) (any, error) {
var v PipelinesByBranchRevisionModuleValue
err := json.Unmarshal(b, &v)
@@ -36,6 +36,7 @@
"branch": "main",
"revision": 12,
"source": 3589,
"end-count": 1,
"commit": "0123456789abcdef0123456789abcdef01234567",
"fp": "fp-0123",
"v": 1,
@@ -109,7 +110,7 @@
}
},
{
"name": "a pipeline ended failed, naming the step that held it",
"name": "a pipeline ended failed, naming the step that held it, its approval not asked",
"of": "event:pipeline-ended",
"subject": "mesh.seat.mesh-pipeline-runner.event.pipeline-ended.nats.main.0123456789abcdef0123456789abcdef01234567",
"given": {
@@ -118,6 +119,7 @@
"branch": "main",
"revision": 12,
"source": 3589,
"end-count": 1,
"commit": "0123456789abcdef0123456789abcdef01234567",
"fp": "fp-0123",
"v": 1,
@@ -134,8 +136,8 @@
{
"step": "approval",
"status": "required",
"outcome": "waiting",
"on": "the operator"
"outcome": "not asked",
"why": "unit of nats:program failed"
}
],
"over-budget": true
@@ -8,6 +8,7 @@
"branch": "main",
"revision": 12,
"source": 3589,
"end-count": 1,
"commit": "0123456789abcdef0123456789abcdef01234567",
"fp": "fp-0123",
"v": 1,
+10 -1
View File
@@ -678,7 +678,7 @@
{
"name": "step-log",
"template": "log",
"max-age-days": 90,
"max-age-days": 100,
"description": "one entry per step's start and end, its output by name; kept on the runner's old streams section",
"key-forms": [
{
@@ -856,6 +856,7 @@
"waiting for a build node",
"did not build",
"accepted",
"not asked",
"declined",
"superseded",
"cancelled"
@@ -974,6 +975,10 @@
"type": "integer",
"description": "the source revision of the watched branch; the pipeline is keyed by it (ADR 0666 section 9)"
},
"end-count": {
"type": "integer",
"description": "how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)"
},
"commit": {
"type": "string",
"description": "the commit the module's entry was last changed at"
@@ -1072,6 +1077,10 @@
"type": "integer",
"description": "the source revision of the watched branch; the pipeline is keyed by it (ADR 0666 section 9)"
},
"end-count": {
"type": "integer",
"description": "how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325)"
},
"commit": {
"type": "string",
"description": "the commit the module's entry was last changed at"
+5 -1
View File
@@ -182,6 +182,8 @@ export interface Pipeline {
"branch": string;
/** the commit the module's entry was last changed at */
"commit": string;
/** how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) */
"end-count"?: number;
"ended"?: string;
/** the module's build-input fingerprint at the commit */
"fp"?: string;
@@ -217,6 +219,8 @@ export interface PipelineEnded {
"branch": string;
/** the commit the module's entry was last changed at */
"commit": string;
/** how often the pipeline has ended: 1 at its first end, one higher at each end again after a passing rerun, so the assigner takes a later end of the same source revision (design 47 G sections 29, 325) */
"end-count"?: number;
"ended"?: string;
/** the module's build-input fingerprint at the commit */
"fp"?: string;
@@ -326,7 +330,7 @@ export interface StepResult {
/** for a waiting step, who or what it waits on: a person, a build node, the revision view */
"on"?: string;
/** never passed for a step that ran nothing (design 47 *Pipeline steps and the policy*) */
"outcome": "running" | "waiting" | "passed" | "failed" | "nothing to run" | "bypassed" | "carried" | "could not run" | "waiting for a build node" | "did not build" | "accepted" | "declined" | "superseded" | "cancelled";
"outcome": "running" | "waiting" | "passed" | "failed" | "nothing to run" | "bypassed" | "carried" | "could not run" | "waiting for a build node" | "did not build" | "accepted" | "not asked" | "declined" | "superseded" | "cancelled";
/** the step's output, by its name in the runner's objects step-outputs */
"output"?: string;
/** the step was asked again by the runner's rerun */
@@ -206,7 +206,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
m1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1,
Merge: "1111111111111111111111111111111111111111", Root: "r0",
Trees: map[string]string{"modules/" + ix: "tree-indexer-2", "modules/a": "tree-a-2"},
Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1"}`},
Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1.0"}`},
Locks: map[string]map[string]string{"modules/" + ix: {"go.sum": "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"}}}
merged := func(f forgemsg.Forge) uint64 {
t.Helper()
@@ -305,10 +305,10 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
{Repository: "novox/mesh-controller", Number: 41, Head: "4141414141414141414141414141414141414141"}}
g1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head,
Merge: "4000000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/c": "tree-c-2"},
Manifests: map[string]string{"modules/c": `{"module":"c","version":"1"}`}, Group: "feat/g", Members: members}
Manifests: map[string]string{"modules/c": `{"module":"c","version":"1.0"}`}, Group: "feat/g", Members: members}
g2 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head,
Merge: "4100000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/k": "tree-k-1"},
Manifests: map[string]string{"modules/k": `{"module":"k","version":"1"}`}, Group: "feat/g", Members: members}
Manifests: map[string]string{"modules/k": `{"module":"k","version":"1.0"}`}, Group: "feat/g", Members: members}
g1Key, groupKey := takein.PendingKey(merged(g1)), takein.GroupTakeInKey("feat/g")
if gathered, ok := said(takein.TakeInPending, groupKey); !ok || gathered.What != takein.TakeInGroup || len(gathered.Merges) != 1 {
t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the group with its first member", groupKey, gathered, ok)
@@ -618,7 +618,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
const merge = "89abcdef0123456789abcdef0123456789abcdef"
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: "0123456789abcdef0123456789abcdef01234567", Merge: merge, Root: "r0", Trees: map[string]string{"modules/a": "newtree"},
Manifests: map[string]string{"modules/a": `{"module":"a","version":"1"}`}})
Manifests: map[string]string{"modules/a": `{"module":"a","version":"1.0"}`}})
if err != nil {
t.Fatal(err)
}
@@ -655,9 +655,9 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
}
const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"
judged := map[string]string{
"good": `{"module":"good","version":"1","resources":[{"id":"d","type":"directory","path":"/srv/good","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
"bad": `{"module":"bad","version":"1","provides":[{"name":"amqp"}],"resources":[{"id":"d","type":"directory","path":"/srv/bad","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
"later": `{"module":"later","version":"1","a-field-from-later":1,"resources":[{"id":"d","type":"directory","path":"/srv/later","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
"good": `{"module":"good","version":"1.0","resources":[{"id":"d","type":"directory","path":"/srv/good","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
"bad": `{"module":"bad","version":"1.0","provides":[{"name":"amqp"}],"resources":[{"id":"d","type":"directory","path":"/srv/bad","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
"later": `{"module":"later","version":"1.0","a-field-from-later":1,"resources":[{"id":"d","type":"directory","path":"/srv/later","mode":"0700"}],"data":{"own":[{"id":"d","path":"${dir:d}","class":"valuable"}]}}`,
}
js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) {
builds.end(t, "novox/mesh-catalog", "avahi", "modules/avahi")
@@ -873,16 +873,16 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
if _, err := js.CreateOrUpdateConsumer(ctx, PreviewsStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-indexer", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
t.Fatal(err)
}
source, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.SourceBucket, History: 4})
if err != nil {
t.Fatal(err)
// The seat mesh-delivery's verb source, as the gate serves it (design 47 G §115): main names the catalogue's trunk head.
answer := func(m *nats.Msg) {
raw, _ := json.Marshal(map[string]any{"result": meshdelivery.SourceAnswer{Branch: "main", Group: "g", Revision: 5,
Repositories: []meshdelivery.SourceHead{{Repository: "novox/mesh-catalog", Head: trunk, Trunk: "main"}}}, "node": "n1"})
_ = m.Respond(raw)
}
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-catalog": trunk}})
if err != nil {
t.Fatal(err)
}
if _, err := source.Put(ctx, meshdeliveryclient.SourceMainKey, data); err != nil {
t.Fatal(err)
for _, subject := range []string{"mesh.seat.mesh-delivery.call.source.>", "mesh.seat.mesh-delivery.tool.source"} {
if _, err := js.Conn().Subscribe(subject, answer); err != nil {
t.Fatal(err)
}
}
})
pull, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: head, Action: forgemsg.PullUpdated,
@@ -22,6 +22,7 @@ import (
"syscall"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -470,22 +471,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
"indexed from it, until the indexer starts again", builds.BuildLogRead, err)
}
started.Store(true)
// The gate's source bucket (mesh-delivery-gate_source) comes with the gate (release 3e, novox/hq ADR 0450 §1): its key
// main is the mesh's source revision, which stamps the modules (issue 1344). Where it does not exist yet the indexer does
// not wait for it (that needs a timer, ADR 0437 §8): it says so, and is started again with the gate.
sourceErrs := make(chan error, 1)
if source := process.OptionalBucket(ctx, js, index.SourceBucket, "no source revision is read until the indexer starts again", logger.Printf); source != nil {
each := func(e jetstream.KeyValueEntry) {
if opOf(e) {
applySource(ctx, ix, e.Key(), e.Revision(), e.Value(), logger)
}
}
if sw, err := settle(ctx, source, each); err != nil {
process.NotWatched(logger.Printf, index.SourceBucket, err, "no source revision is read until the indexer starts again")
} else {
go func() { sourceErrs <- follow(ctx, sw, each) }()
}
}
// The source revision of a watched branch (design 47 G §15, §115, §362): asked of the seat mesh-delivery's verb source as a
// kept call, once at the start, before the handler of its event source-moved reads anything; a source-moved heard
// meanwhile is read after.
source := &sourceFollow{read: sourceVia(calls.CallKept), apply: ix.ApplySource, logf: logger.Printf}
go source.catchUp(ctx)
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
// assignments is settled first, so the toolchains settle through the gate already knowing it.
gate := buildrun.NewToolchainGate(sortedModules(), ix.PutToolchain, logger.Printf)
@@ -668,6 +658,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}); err != nil {
return err
}
if err := hub.Handle(meshdelivery.SubjectSourceMovedAll, func(m jetstream.Msg) { source.heard(ctx, m) }); err != nil {
return err
}
hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4)
versions := func(m jetstream.Msg) {
if !heardVersion(ctx, ix, m, logger.Printf) {
@@ -753,8 +746,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return nil
case err := <-errs:
return err
case err := <-sourceErrs:
return err
case err := <-assignmentsErrs:
return err
case err := <-tools.errs:
@@ -5,7 +5,9 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"reflect"
"strings"
"testing"
"time"
@@ -16,7 +18,6 @@ import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -237,24 +238,64 @@ type opener struct{ err error }
func (o opener) KeyValue(context.Context, string) (jetstream.KeyValue, error) { return nil, o.err }
func TestTheSourceMainKeyIsTakenAtItsRevision(t *testing.T) { // 1344/AC1 (the watch's side: key, revision, value)
var logs bytes.Buffer
logger := log.New(&logs, "", 0)
mods := &memStore{vals: map[string][]byte{}}
ix := &index.Indexer{Store: &memStore{vals: map[string][]byte{}}, Modules: mods, Builds: index.NewBuilds()}
value, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-delivery": head}})
if err != nil {
t.Fatal(err)
func TestTheSourceIsReadFromTheSeatAndASourceMovedDuringTheCatchUpIsReadAfterIt(t *testing.T) { // design 47 G §15, §115, §362
var order []string
revision := int64(4)
var logs []string
var s *sourceFollow
s = &sourceFollow{
read: func(branch string) (meshdelivery.SourceAnswer, error) {
order = append(order, "read "+branch)
if revision == 0 {
return meshdelivery.SourceAnswer{}, fmt.Errorf("source.main is not readable")
}
if len(order) == 1 { // a source-moved is heard while the catch-up reads
s.moved(context.Background(), "main")
s.moved(context.Background(), "main")
s.moved(context.Background(), "beta")
}
return meshdelivery.SourceAnswer{Branch: branch, Revision: revision}, nil
},
apply: func(_ context.Context, a meshdelivery.SourceAnswer) error {
order = append(order, fmt.Sprintf("apply %s %d", a.Branch, a.Revision))
return nil
},
logf: func(f string, a ...any) { logs = append(logs, fmt.Sprintf(f, a...)) },
}
applySource(context.Background(), ix, "other", 3, value, logger) // not the key of the mesh's source
applySource(context.Background(), ix, meshdeliveryclient.SourceMainKey, 4, []byte(`{"v":1`), logger)
if !strings.Contains(logs.String(), "source") {
t.Errorf("a source that cannot be read was not said: %q", logs.String())
s.catchUp(context.Background())
want := []string{"read main", "apply main 4", "read main", "apply main 4"} // once, after the catch-up; beta is left
if !reflect.DeepEqual(order, want) {
t.Errorf("order = %v, want %v", order, want)
}
logs.Reset()
applySource(context.Background(), ix, meshdeliveryclient.SourceMainKey, 5, value, logger)
if logs.Len() != 0 {
t.Errorf("a good source said %q", logs.String())
s.moved(context.Background(), "main") // after the catch-up: read at once
if len(order) != 6 {
t.Errorf("a source-moved after the catch-up was not read at once: %v", order)
}
order, revision = nil, 0
s.moved(context.Background(), "main")
if len(order) != 1 || len(logs) != 1 || !strings.Contains(logs[0], "not readable") {
t.Errorf("a source that cannot be read: order %v, said %v, want it said and nothing applied", order, logs)
}
}
func TestTheSeatsSourceAnswerIsReadAndItsErrorIsAnError(t *testing.T) { // design 47 G §115
var asked any
read := sourceVia(func(key string, body any) (json.RawMessage, error) {
asked = body
if key != "seat:mesh-delivery.source" {
t.Errorf("asked %q", key)
}
if body.(meshdelivery.SourceInput).Branch == "bad" {
return json.RawMessage(`{"error":"source.bad is not readable"}`), nil
}
return json.RawMessage(`{"branch":"main","revision":7,"repositories":[{"repository":"novox/mesh-delivery","head":"` + head + `","trunk":"main"}]}`), nil
})
a, err := read("main")
if err != nil || a.Revision != 7 || len(a.Repositories) != 1 || asked.(meshdelivery.SourceInput).Branch != "main" {
t.Errorf("source = %+v, %v", a, err)
}
if _, err := read("bad"); err == nil || !strings.Contains(err.Error(), "not readable") {
t.Errorf("an answer with an error was no error: %v", err)
}
}
@@ -226,7 +226,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
if got, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, ""); err != nil || func() string { raw, _ := json.Marshal(got); return string(raw) }() != `{"revision":0,"seats":[]}` {
t.Errorf("seat-definitions of an indexer that kept none = %+v, %v", got, err)
}
contracts := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
contracts := `{"module":"mesh-contracts","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
merged("novox/mesh-contracts", 80, contractsMerge, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}})
entry := func(key string, ok func(index.SeatDefinitionEntry) bool) index.SeatDefinitionEntry {
@@ -2,26 +2,100 @@ package main
import (
"context"
"log"
"encoding/json"
"fmt"
"strings"
"sync"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"mesh-delivery-indexer/internal/index"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"github.com/nats-io/nats.go/jetstream"
)
// applySource takes one put of the gate's bucket source into the indexer: the key main, at its revision, which is the mesh's
// source revision (mesh-sdk go/delivery SourceMainKey, Source). Any other key is not the mesh's source. A value that is no
// source is said; so is a write that failed, and it is tried again only at the next revision (novox/hq ADR 0437 §8).
func applySource(ctx context.Context, ix *index.Indexer, key string, rev uint64, value []byte, logger *log.Logger) {
if key != meshdeliveryclient.SourceMainKey {
// sourceInvoke is the seat mesh-delivery's verb source as the manifest's invokes names it: a watched branch's source
// revision, asked of the seat and never read from the gate's state (design 47 G §15, §115; ADR 0596 §2).
const sourceInvoke = "seat:" + meshdelivery.Seat + "." + meshdelivery.VerbSource
// sourceVia asks the seat for one branch's source revision through call (process.Conn.CallKept, a read) and reads its
// declared answer; an answer that carries an error is that error.
func sourceVia(call toolCaller) func(branch string) (meshdelivery.SourceAnswer, error) {
return func(branch string) (meshdelivery.SourceAnswer, error) {
raw, err := call(sourceInvoke, meshdelivery.SourceInput{Branch: branch})
if err != nil {
return meshdelivery.SourceAnswer{}, err
}
var a meshdelivery.SourceAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return a, fmt.Errorf("the answer of %s is not a source revision: %w", sourceInvoke, err)
}
if a.Error != "" {
return a, fmt.Errorf("%s: %s", sourceInvoke, a.Error)
}
return a, nil
}
}
// sourceFollow takes the seat's source revisions into the indexer: first the catch-up of the heads the source names, then
// each source-moved the seat says (design 47 G §362). A source-moved heard before the catch-up ended is applied after it,
// once per branch.
type sourceFollow struct {
read func(branch string) (meshdelivery.SourceAnswer, error)
apply func(context.Context, meshdelivery.SourceAnswer) error
logf func(string, ...any)
mu sync.Mutex
caught bool
waiting map[string]bool
}
// catchUp reads the trunk branch's source revision and applies it, then the branches a source-moved named meanwhile. A
// read that fails is said; the catch-up ends all the same, and the next source-moved reads again.
func (s *sourceFollow) catchUp(ctx context.Context) {
s.take(ctx, trunkBranch)
s.mu.Lock()
s.caught = true
branches := s.waiting
s.waiting = nil
s.mu.Unlock()
for branch := range branches {
s.take(ctx, branch)
}
}
// moved is the handler of one source-moved on branch: held until the catch-up ends, read at once after.
func (s *sourceFollow) moved(ctx context.Context, branch string) {
s.mu.Lock()
if !s.caught {
if s.waiting == nil {
s.waiting = map[string]bool{}
}
s.waiting[branch] = true
s.mu.Unlock()
return
}
s, err := meshdeliveryclient.DecodeSource(value)
s.mu.Unlock()
s.take(ctx, branch)
}
// take reads one branch's source revision and applies it. A branch other than the trunk's is left until the indexer keeps
// a catalog graph per watched branch.
func (s *sourceFollow) take(ctx context.Context, branch string) {
if branch != trunkBranch {
return
}
a, err := s.read(branch)
if err != nil {
logger.Printf("source.%s at revision %d is not a source, and is left: %v", key, rev, err)
s.logf("source.%s is not read, and is read again at its next source-moved: %v", branch, err)
return
}
if err := s.apply(ctx, a); err != nil {
s.logf("source.%s at revision %d could not be taken into the modules: %v", branch, a.Revision, err)
}
if err := ix.ApplySource(ctx, rev, s); err != nil {
logger.Printf("source.%s at revision %d could not be taken into the modules: %v", key, rev, err)
}
// heard is the hub's handler of source-moved: the branch is the last token of the subject. The message is acknowledged at once.
func (s *sourceFollow) heard(ctx context.Context, m jetstream.Msg) {
defer func() { _ = m.Ack() }()
if branch := m.Subject()[strings.LastIndex(m.Subject(), ".")+1:]; branch != "" {
s.moved(ctx, branch)
}
}
@@ -76,8 +76,8 @@ func TestTheControlRecords(t *testing.T) {
}
modules[name] = &views.Entry{Value: raw, Revision: 1}
}
asks := `{"module":"cache","version":"1","requires":[{"name":"c","storage":"key-value","template":"state"}]}`
for digest, raw := range map[string]string{dAsk: asks, dNone: `{"module":"plain","version":"1"}`, dRefused: asks} {
asks := `{"module":"cache","version":"1.0","requires":[{"name":"c","storage":"key-value","template":"state"}]}`
for digest, raw := range map[string]string{dAsk: asks, dNone: `{"module":"plain","version":"1.0"}`, dRefused: asks} {
put(manifests, digest, 1, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "m", Manifest: raw})
}
putVerdict(dAsk, 7, meshcatalog.VerdictsByDigestValue{Module: "cache", State: meshcatalogclient.VerdictAccepted, Judge: "j"})
@@ -127,7 +127,7 @@ func TestTheControlRecords(t *testing.T) {
// raises generated-types-out-of-step from a trunk build end, cleared only by a clean one of the same module.
t.Run("seat definitions", func(t *testing.T) {
const repo, m1, m2, m3, head = "novox/mesh-contracts", "m1", "m2", "m3", "h1"
manifest := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
manifest := `{"module":"mesh-contracts","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}`
files := map[string]map[string]string{
m1: {"seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)},
@@ -162,7 +162,7 @@ func TestTheControlRecords(t *testing.T) {
merged := func(merge string) {
t.Helper()
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Number: 1, Merge: merge,
Manifests: map[string]string{meshcatalogclient.RootPath: manifest, "modules/other": `{"module":"other","version":"1"}`}}); err != nil {
Manifests: map[string]string{meshcatalogclient.RootPath: manifest, "modules/other": `{"module":"other","version":"1.0"}`}}); err != nil {
t.Fatal(err)
}
}
@@ -284,7 +284,7 @@ func TestTheControlRecords(t *testing.T) {
// Ruling (lead, on A2i's finding 3): indexer_seat_definitions_again for a pull request reads the head's own manifest's
// generated-from: a trunk that names none, a head that names it, is read as the contracts module the head is.
ix.Modules, ix.Manifests = newMem(), newMem()
plain := `{"module":"mesh-contracts","version":"1"}`
plain := `{"module":"mesh-contracts","version":"1.0"}`
kept, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mesh-contracts", Manifest: plain})
mkey, _ := meshcatalogclient.ManifestsKey(meshcatalogclient.ManifestDigest([]byte(plain)))
ix.Manifests.(*memStore).vals[mkey] = kept
@@ -425,7 +425,7 @@ func TestTheControlRecords(t *testing.T) {
}
return nil, nil
}}
sdk := `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` +
sdk := `{"module":"mesh-sdk","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` +
`{"name":"ts","kind":"package","language":"typescript","paths":["src","package.json"],"series":"0.3"}]}}`
if err := ix.KeepPackageNames(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Merge: "m1",
Manifests: map[string]string{meshcatalogclient.RootPath: sdk}}); err != nil {
@@ -498,11 +498,11 @@ func TestTheControlRecords(t *testing.T) {
pkgs, imports []string
pinned string
}{
"new": {"novox/mesh-catalog", `{"module":"new","version":"1","packages":{"git.novox.be/novox/mesh-sdk/go":"0.2","@novox/ui":"1.0"}}`, nil, nil, ""},
"old": {"novox/mesh-catalog", `{"module":"old","version":"1"}`, []string{sdkGo}, nil, "v0.1.15"},
"imports": {"novox/mesh-tools", `{"module":"imports","version":"1"}`, nil, []string{sdkGo + "/views"}, ""},
"sibling": {"novox/mesh-sdk", `{"module":"sibling","version":"1"}`, []string{sdkGo}, nil, ""},
"none": {"novox/mesh-catalog", `{"module":"none","version":"1"}`, nil, nil, ""},
"new": {"novox/mesh-catalog", `{"module":"new","version":"1.0","packages":{"git.novox.be/novox/mesh-sdk/go":"0.2","@novox/ui":"1.0"}}`, nil, nil, ""},
"old": {"novox/mesh-catalog", `{"module":"old","version":"1.0"}`, []string{sdkGo}, nil, "v0.1.15"},
"imports": {"novox/mesh-tools", `{"module":"imports","version":"1.0"}`, nil, []string{sdkGo + "/views"}, ""},
"sibling": {"novox/mesh-sdk", `{"module":"sibling","version":"1.0"}`, []string{sdkGo}, nil, ""},
"none": {"novox/mesh-catalog", `{"module":"none","version":"1.0"}`, nil, nil, ""},
} {
digest := fmt.Sprintf("%064x", len(name)*16+int(name[0]))
putJSON(manifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: name, Manifest: u.manifest})
@@ -569,7 +569,7 @@ func TestTheControlRecords(t *testing.T) {
ix, _, _ := manifestsIndexer(t, rec(t, "build-1", "avahi", catalogueRepo, "modules/avahi", "main", "", "2026-10-19T09:00:00Z", aInputs))
cands := newMem()
ix.ConfigCandidates, ix.Now = cands, func() time.Time { return at }
trunk := `{"module":"avahi","version":"1","config":{"port":{"type":"integer","default":5353,"why":"mDNS"}}}`
trunk := `{"module":"avahi","version":"1.0","config":{"port":{"type":"integer","default":5353,"why":"mDNS"}}}`
if err := ix.Apply(ctx, withManifests(mergedMsg(catalogueRepo, map[string]string{"modules/avahi": "t1"}), map[string]string{"modules/avahi": trunk}, nil)); err != nil {
t.Fatal(err)
}
@@ -34,10 +34,10 @@ func (b fakeBounds) Disarm(_ context.Context, key string) error { delete(b, key)
func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) {
ctx := context.Background()
const dir, dockerDir = "modules/distribution", "modules/docker"
old := `{"module":"distribution","version":"1","listens":[{"name":"registry","port":5000,"from":"mesh"}]}`
next := `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry","scope":"mesh"}],` +
old := `{"module":"distribution","version":"1.0","listens":[{"name":"registry","port":5000,"from":"mesh"}]}`
next := `{"module":"distribution","version":"1.0","claims":[{"name":"container-image-registry","scope":"mesh"}],` +
`"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`
docker := `{"module":"docker","version":"1","resources":[{"id":"daemon","type":"file","path":"/etc/docker/daemon.json","template":"files/daemon.json"}]}`
docker := `{"module":"docker","version":"1.0","resources":[{"id":"daemon","type":"file","path":"/etc/docker/daemon.json","template":"files/daemon.json"}]}`
dockerNeeds, _ := catalogue.ModuleNeeds([]byte(docker), map[string]string{"files/daemon.json": `"${seat:container-image-registry:reach}"`})
if needs, _ := catalogue.ManifestNeeds([]byte(next)); !slices.Contains(needs, "form:seat-reach") || !slices.Contains(dockerNeeds, "form:seat-reach") {
t.Fatalf("the new manifest needs %v and docker's %v, want form:seat-reach in both (mesh-sdk ecdbf1a, ModuleNeeds)", needs, dockerNeeds)
@@ -185,7 +185,7 @@ func TestModuleOfADirectory(t *testing.T) { // 1238/AC3, as R4e has it: the modu
ctx := context.Background()
// The trunk holds modules/odd as the module odd-name (its manifest names it so); modules/plain is held by none.
merged := mergedMsg("novox/mesh-delivery", map[string]string{"modules/odd": "t"})
merged.Manifests["modules/odd"] = `{"module":"odd-name","version":"1"}`
merged.Manifests["modules/odd"] = `{"module":"odd-name","version":"1.0"}`
if err := ix.Apply(ctx, merged); err != nil {
t.Fatal(err)
}
@@ -202,7 +202,7 @@ func TestModuleOfADirectory(t *testing.T) { // 1238/AC3, as R4e has it: the modu
}
// A manifest the pull message carries names its module, whatever directory it is in.
f := pull("novox/mesh-delivery", 13, "feat/y", h2, "modules/odd")
f.Manifests = map[string]string{"modules/odd": `{"module":"renamed","version":"1"}`}
f.Manifests = map[string]string{"modules/odd": `{"module":"renamed","version":"1.0"}`}
if name := moduleOfDir(held, f, "modules/odd"); name != "renamed" {
t.Errorf("modules/odd = %q, want the module its manifest names", name)
}
@@ -14,7 +14,6 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
@@ -82,7 +81,7 @@ func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2
func mergedMsg(repo string, trees map[string]string) forgemsg.Forge {
manifests := map[string]string{}
for dir := range trees {
manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}`
manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1.0"}`
}
return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
}
@@ -160,7 +159,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
if err := ix.Apply(context.Background(), m); err != nil {
return err
}
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
if err := applyS(ix, context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
ix.Taken = on
@@ -169,7 +168,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
{name: "a source head whose merge was not applied is not a taken key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: "",
then: func(ix *Indexer) error { // the forge stream still holds the merge: the consumer is not drained
delete(ix.Taken.(*memStore).vals, takenKey(repo))
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
if err := applyS(ix, context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
return ix.FillTaken(context.Background())
@@ -180,7 +179,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
{name: "a source head of a drained forge stream whose merge was never heard leaves the key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
then: func(ix *Indexer) error {
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { return commit == merge && in == next, nil }
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
if err := applyS(ix, context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
return ix.FillTaken(context.Background())
@@ -188,7 +187,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
{name: "a source head of a drained forge stream fills no missing key", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: "",
then: func(ix *Indexer) error {
delete(ix.Taken.(*memStore).vals, takenKey(repo))
if err := ix.ApplySource(context.Background(), 8, sourceOf(repo, next)); err != nil {
if err := applyS(ix, context.Background(), 8, sourceOf(repo, next)); err != nil {
return err
}
return ix.FillTaken(context.Background())
@@ -300,7 +299,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
}},
{name: "a later source revision of another repository's merge", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
then: func(ix *Indexer) error {
return ix.ApplySource(context.Background(), 9, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{repo: merge, "novox/mesh-catalog": otherMerge}})
return applyS(ix, context.Background(), 9, srcT{Group: "g", Heads: map[string]string{repo: merge, "novox/mesh-catalog": otherMerge}})
}},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -311,7 +310,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
if err := ix.Apply(ctx, mergedMsg(repo, three)); err != nil {
t.Fatal(err)
}
if err := ix.ApplySource(ctx, 7, sourceOf(repo, merge)); err != nil {
if err := applyS(ix, ctx, 7, sourceOf(repo, merge)); err != nil {
t.Fatal(err)
}
writes := mods.writes
@@ -379,7 +378,7 @@ func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *t
t.Errorf("after the merge %s = %+v (%v), want path %q", module, m, ok, want)
}
}
if err := ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil {
if err := applyS(ix, ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil {
t.Fatal(err)
}
if err := ix.Recompute(ctx); err != nil {
@@ -515,7 +514,7 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) {
if repository != to || sha != toHead || dir != "modules/a" {
t.Errorf("read %s at %s %q, want only %s at %s modules/a", repository, sha, dir, to, toHead)
}
return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1"}`}}, nil
return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1.0"}`}}, nil
}
}
for _, f := range tc.merges {
@@ -106,7 +106,7 @@ func TestTheIndexerAsksTheForgeForTheNamedPathsObjectsOfAMerge(t *testing.T) { /
return map[string]map[string]string{"": sdkObjects}, nil
}
raw := fixture(t, "sdk-9cb4d59-mesh-sdk")
f := withManifests(mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree", "modules/a": "t"}), map[string]string{"": raw, "modules/a": `{"module":"a","version":"1"}`}, nil)
f := withManifests(mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree", "modules/a": "t"}), map[string]string{"": raw, "modules/a": `{"module":"a","version":"1.0"}`}, nil)
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
@@ -40,7 +40,6 @@ const (
AssignmentsState = "assignments"
AssignmentsBucket = AssignerModule + "_" + AssignmentsState
AssignmentsHistory = 5
SourceBucket = GateModule + "_source"
PreviewsBucket = CheckerModule + "_previews"
PreviewsHistory = 5
toolchainsState = "toolchains"
@@ -365,7 +365,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
f.Locks[d] = l
}
f.Locks[contracts] = map[string]string{}
f.Manifests[contracts] = `{"module":"` + row.module + `","version":"1","build":{"artifacts":[{"name":"go-seats","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
f.Manifests[contracts] = `{"module":"` + row.module + `","version":"1.0","build":{"artifacts":[{"name":"go-seats","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
b.ix.Files = func(_ context.Context, repository, commit string, dirs []string) (map[string]map[string]string, error) {
if repository != f.Repository || commit != f.Merge || !slices.Equal(dirs, []string{contracts + "/seats"}) {
t.Errorf("listed %v of %s at %s", dirs, repository, commit)
@@ -17,7 +17,6 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
@@ -39,7 +38,7 @@ func previewBed(t *testing.T, report bool) (*Indexer, *memStore, *memStore) {
if report {
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
}
if err := ix.ApplySource(context.Background(), 5, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{catalogueRepo: trunkNow}}); err != nil {
if err := applyS(ix, context.Background(), 5, srcT{Group: "g", Heads: map[string]string{catalogueRepo: trunkNow}}); err != nil {
t.Fatal(err)
}
return ix, mods, ix.Store.(*memStore)
@@ -205,7 +204,7 @@ func TestAPreviewMadeOntoAnOldTrunkIsOutOfDate(t *testing.T) { // 1445/AC3
if got := fpOf(t, cands); got.Fp2 == "" {
t.Fatalf("after the right preview: %+v", got)
}
if err := ix.ApplySource(context.Background(), 6, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{catalogueRepo: "8888888888888888888888888888888888888888"}}); err != nil {
if err := applyS(ix, context.Background(), 6, srcT{Group: "g", Heads: map[string]string{catalogueRepo: "8888888888888888888888888888888888888888"}}); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.None2 != NonePreviewStale {
@@ -9,13 +9,12 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the grants part's gaps: modules registered from their own repositories had no manifest here
const forge = "http://novox.internal:20000/jschoubben/snake.git"
snake := `{"module":"snake","version":"1"}`
snake := `{"module":"snake","version":"1.0"}`
for _, tc := range []struct {
name string
path string
@@ -41,7 +40,7 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran
{wantRead: true, name: "no manifest at the path", path: "modules/x",
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
{wantRead: true, name: "a manifest of another module", path: "",
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"other","version":"1"}`}}},
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"other","version":"1.0"}`}}},
{wantRead: true, name: "a repository whose default branch is master", path: "", ref: "master", want: true,
read: RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": snake}}},
{wantRead: true, name: "a repository built from a branch of its own (nox-mesh)", path: "", ref: "nox-mesh", want: true,
@@ -151,7 +150,7 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran
var asked []string
ix.Read = func(_ context.Context, _, sha, dir string) (RepoRead, error) {
asked = append(asked, sha+":"+dir)
return RepoRead{Root: "r1", Trees: map[string]string{"modules/a": "treeA2"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"2"}`}}, nil
return RepoRead{Root: "r1", Trees: map[string]string{"modules/a": "treeA2"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"2.0"}`}}, nil
}
got := ix.TakeIn(ctx, "a")
m, ok := moduleOf(t, mods, "a")
@@ -202,7 +201,7 @@ func TestARegistrationOfAnotherRepositoryDoesNotHideTheRealOne(t *testing.T) { /
ix.Read = func(_ context.Context, repo, _, dir string) (RepoRead, error) {
asked = append(asked, repo+":"+dir)
if repo == "novox/Link2Pay" {
return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"link2pay","version":"1"}`}}, nil
return RepoRead{Root: "r0", Trees: map[string]string{"": "t1"}, Manifests: map[string]string{"": `{"module":"link2pay","version":"1.0"}`}}, nil
}
return RepoRead{Root: "r0"}, nil
}
@@ -221,7 +220,7 @@ func TestARegistrationOfAnotherRepositoryDoesNotHideTheRealOne(t *testing.T) { /
func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) { // a timeout or a 404 at the head: no read at an older commit
// Issue 1945: car-hunter and snake answered 404 until the forge's account could read them, and were read again only when
// the indexer started again. Each row's trigger must now take the module in once the forge reads it.
const snake = `{"module":"snake","version":"1"}`
const snake = `{"module":"snake","version":"1.0"}`
ctx := context.Background()
for _, tc := range []struct{ failure, trigger string }{
{"timeout", "the indexer starts again"},
@@ -261,7 +260,7 @@ func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) {
t.Fatal(err)
}
case "a new source revision":
if err := ix.ApplySource(ctx, 2, meshdelivery.SourceByMainValue{Heads: map[string]string{"jschoubben/snake": h2, "novox/other": h1}}); err != nil {
if err := applyS(ix, ctx, 2, srcT{Heads: map[string]string{"jschoubben/snake": h2, "novox/other": h1}}); err != nil {
t.Fatal(err)
}
ix.retryWG.Wait() // read again in the background
@@ -281,7 +280,7 @@ func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) {
}
// Taken in, it is read no more: a further event asks the forge nothing.
n := len(asked)
if err := ix.ApplySource(ctx, 3, meshdelivery.SourceByMainValue{Heads: map[string]string{"jschoubben/snake": h2}}); err != nil {
if err := applyS(ix, ctx, 3, srcT{Heads: map[string]string{"jschoubben/snake": h2}}); err != nil {
t.Fatal(err)
}
ix.retryWG.Wait()
@@ -100,7 +100,7 @@ func TestAPathWithoutAModuleJSONIsNeverAModule(t *testing.T) { // 1900/AC2: test
func TestStartingDeletesTheKeysTheTrunkDoesNotHold(t *testing.T) { // 1900/AC3
ix, mods := twoModules(t)
ctx := context.Background()
if err := ix.ApplySource(ctx, 3, sourceOf(catalogueRepo, merge)); err != nil {
if err := applyS(ix, ctx, 3, sourceOf(catalogueRepo, merge)); err != nil {
t.Fatal(err)
}
var asked string
@@ -126,7 +126,7 @@ func TestStartingDeletesTheKeysTheTrunkDoesNotHold(t *testing.T) { // 1900/AC3
func TestStartingDeletesNothingWhenTheTrunkCannotBeRead(t *testing.T) { // 1900/AC3: failure is loud, never a mass delete
ix, mods := twoModules(t)
ctx := context.Background()
if err := ix.ApplySource(ctx, 3, sourceOf(catalogueRepo, merge)); err != nil {
if err := applyS(ix, ctx, 3, sourceOf(catalogueRepo, merge)); err != nil {
t.Fatal(err)
}
for name, trunk := range map[string]func(context.Context, string, string) ([]string, error){
@@ -146,7 +146,7 @@ func TestAKeyWrittenAheadOfTheSourceHeadStaysAndOneWithoutAPathIsSkipped(t *test
ix, mods := twoModules(t) // written at the merge commit C
ctx := context.Background()
const older = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
if err := ix.ApplySource(ctx, 3, sourceOf(catalogueRepo, older)); err != nil {
if err := applyS(ix, ctx, 3, sourceOf(catalogueRepo, older)); err != nil {
t.Fatal(err)
}
mods.put("nopath", mustEncode(t, meshcatalog.ModulesByModuleValue{Repository: catalogueRepo, Commit: older, None2: "x", Manifest: strings.Repeat("a", 64)}))
@@ -11,8 +11,9 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// The gate's source revision (novox/hq ADR 0437 §5, ADR 0450 §1): source.main names every repository's merged head, and its
// revision in the bucket is the mesh's source revision. The indexer keeps the latest it has seen. A module is stamped
// The gate's source revision of a watched branch (design 47 G §15, §115), read through the seat mesh-delivery's verb source
// and its event source-moved, never the gate's state: it names every repository's merged head, and its revision is the
// mesh's source revision of that branch. The indexer keeps the latest it has seen. A module is stamped
// with it (Module.Src) when the head the source names for its repository is the merge commit the module's fingerprint
// was computed at: from the modules bucket when the merge was heard first, and when the merge is heard after the source.
@@ -33,12 +34,17 @@ func (ix *Indexer) srcOf(repository, merge string) uint64 {
return 0
}
// ApplySource takes a revision of the gate's source.main in. An older revision than the latest seen is left. For each
// ApplySource takes the source revision the verb source answered in. An older revision than the latest seen is left. For each
// repository the source names, each module built from it whose fingerprint was computed at the head the source names, and
// that carries no Src yet, is written again with Src the revision (once: a later revision leaves it); a module computed at another commit is left (a merge heard later is stamped
// when it is written, applyMerged), and a repository none of the indexer's modules is built from writes nothing. The
// modules of a repository are those the modules bucket holds of it (ADR 0501 clause 6).
func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s meshdelivery.SourceByMainValue) error {
func (ix *Indexer) ApplySource(ctx context.Context, a meshdelivery.SourceAnswer) error {
rev := uint64(a.Revision)
s := struct{ Heads map[string]string }{Heads: map[string]string{}}
for _, r := range a.Repositories {
s.Heads[r.Repository] = r.Head
}
ix.kickJudge(ctx)
ix.mu.Lock()
if rev < ix.source.rev {
@@ -10,8 +10,20 @@ import (
const otherMerge = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
func sourceOf(repo, head string) meshdelivery.SourceByMainValue {
return meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{repo: head}}
// srcT is a source revision's heads as the tests write them; applyS says it as the verb source answers it.
type srcT struct {
Group string
Heads map[string]string
}
func sourceOf(repo, head string) srcT { return srcT{Group: "g", Heads: map[string]string{repo: head}} }
func applyS(ix *Indexer, ctx context.Context, rev uint64, s srcT) error {
a := meshdelivery.SourceAnswer{Branch: "main", Group: s.Group, Revision: int64(rev)}
for r, h := range s.Heads {
a.Repositories = append(a.Repositories, meshdelivery.SourceHead{Repository: r, Head: h, Trunk: "main"})
}
return ix.ApplySource(ctx, a)
}
func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.T) { // 1344/AC1
@@ -25,8 +37,8 @@ func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.
t.Fatalf("module before the source = %+v, want Src 0", before)
}
// The source names the head merged for the delivery repository, and another repository's head no module of ours is built from.
s := meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-delivery": merge, "novox/mesh-catalog": otherMerge}}
if err := ix.ApplySource(ctx, 7, s); err != nil {
s := srcT{Group: "g", Heads: map[string]string{"novox/mesh-delivery": merge, "novox/mesh-catalog": otherMerge}}
if err := applyS(ix, ctx, 7, s); err != nil {
t.Fatal(err)
}
m, ok := moduleOf(t, mods, "a")
@@ -39,10 +51,10 @@ func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.
t.Errorf("module c = %+v: written for a head no merge message of the indexer carried", c)
}
// The same revision again changes nothing, and so does a later one: Src is the first revision naming the commit.
if err := ix.ApplySource(ctx, 7, s); err != nil {
if err := applyS(ix, ctx, 7, s); err != nil {
t.Fatal(err)
}
if err := ix.ApplySource(ctx, 9, s); err != nil {
if err := applyS(ix, ctx, 9, s); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "a"); m.Src != 7 {
@@ -56,15 +68,15 @@ func TestAMergeHeardAfterTheSourceNamesItsHeadIsStampedWhenWritten(t *testing.T)
ix, mods := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs))
ctx := context.Background()
_ = ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree"}))
_ = ix.ApplySource(ctx, 9, sourceOf("novox/mesh-delivery", merge))
_ = ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", merge))
_ = applyS(ix, ctx, 9, sourceOf("novox/mesh-delivery", merge))
_ = applyS(ix, ctx, 7, sourceOf("novox/mesh-delivery", merge))
if m, _ := moduleOf(t, mods, "a"); m.Src != 9 {
t.Errorf("module a Src = %d, want 9", m.Src)
}
})
ix, mods := modulesIndexer(t, rec(t, "build-1", "a", "novox/mesh-delivery", "modules/a", "main", "", "2026-10-19T09:00:00Z", aInputs))
ctx := context.Background()
if err := ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil {
if err := applyS(ix, ctx, 7, sourceOf("novox/mesh-delivery", merge)); err != nil {
t.Fatal(err)
}
if _, ok := moduleOf(t, mods, "a"); ok {
@@ -89,7 +101,7 @@ func TestAMergeThatNoSourceRevisionNamesIsWrittenAsToday(t *testing.T) { // 1344
t.Fatalf("module a = %+v (%v), want the merge written with Src 0", m, ok)
}
// A source revision that names another head of the repository: a merge of the head it does not name keeps Src 0.
if err := ix.ApplySource(ctx, 7, sourceOf("novox/mesh-delivery", otherMerge)); err != nil {
if err := applyS(ix, ctx, 7, sourceOf("novox/mesh-delivery", otherMerge)); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "a"); m.Src != 0 {
@@ -15,7 +15,6 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
@@ -38,7 +37,7 @@ func renamed(t *testing.T, from, to string) string {
if out == raw {
t.Fatalf("%s's manifest has no module line", from)
}
return out
return strings.Replace(out, `"version": "1",`, `"version": "1.0",`, 1) // the validator reads MAJOR.MINOR only (design 47 G §368)
}
func standingOn(t *testing.T, raw, base string) string {
@@ -150,8 +149,8 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14
}
// The release that last merged avahi: its source revision names avahi's commit, so Src (and FPSrc) are 7.
merged, _ := moduleOf(t, mods, "avahi")
src := meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{merged.Repository: merged.Commit}}
if err := ix.ApplySource(context.Background(), 7, src); err != nil {
src := srcT{Group: "g", Heads: map[string]string{merged.Repository: merged.Commit}}
if err := applyS(ix, context.Background(), 7, src); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "avahi"); m.Src != 7 || m.FpSrc != 7 {
@@ -183,7 +182,7 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14
if m.FpSrc != 0 {
t.Errorf("a fingerprint moved by a recompute: fp-src %d, want 0", m.FpSrc)
}
if err := ix.ApplySource(context.Background(), 8, src); err != nil {
if err := applyS(ix, context.Background(), 8, src); err != nil {
t.Fatal(err)
}
if again, _ := moduleOf(t, mods, "avahi"); again.FpSrc != 0 {
@@ -344,7 +343,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
// verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge.
invoking := func(name, invokes string) mod {
raw := renamed(t, "avahi", name)
return mod{name, strings.Replace(raw, `"version": "1",`, `"version": "1", "invokes": [`+invokes+`],`, 1)}
return mod{name, strings.Replace(raw, `"version": "1.0",`, `"version": "1.0", "invokes": [`+invokes+`],`, 1)}
}
vx, _ := newFormIndexer(t, invoking("caller", `"callee.avahi_status"`), invoking("callee", `"caller.avahi_browse", "*"`),
invoking("third", `"callee.avahi_status"`))
@@ -505,7 +504,7 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) {
sdkMerge := func(commit, tree string) meshcatalog.ModulesByModuleValue {
t.Helper()
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1,
Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1"}`}}
Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1.0"}`}}
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
@@ -40,8 +40,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
ix.Modules.(*memStore).put(module, value)
return digest
}
good := hold("good", `{"module":"good","version":"1"}`)
bad := hold("bad", `{"module":"bad","version":"1","requires":["amqp"]}`)
good := hold("good", `{"module":"good","version":"1.0"}`)
bad := hold("bad", `{"module":"bad","version":"1.0","requires":["amqp"]}`)
// Both taken in by a merge: judged in full.
j, err := ix.JudgeAll(ctx, map[string]bool{good: true, bad: true})
if err != nil || j.Judged != 2 || j.Written != 2 || len(j.Refused) != 1 {
@@ -91,7 +91,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("the new indexer at its start: %+v %v, said %d, want both manifests judged and nothing written", j, err, len(said))
}
// The manifest mended: its new digest is accepted and the module's refusal goes.
mended := hold("bad", `{"module":"bad","version":"2"}`)
mended := hold("bad", `{"module":"bad","version":"2.0"}`)
if j, err := ix.JudgeAll(ctx, map[string]bool{mended: true}); err != nil || j.Written != 1 || len(j.Refused) != 0 {
t.Fatalf("mended: %+v %v", j, err)
}
@@ -101,7 +101,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// ADR 0579 §1: a manifest held before any verdict (taken in under the judges until then), which this judge finds a
// problem in, stays accepted with the problem as a warning, carried in its value and said nowhere as a refusal; the
// same problem in a manifest a merge takes in now, or in one whose digest changed, is refused in full.
old := hold("old", `{"module":"old","version":"1","requires":["amqp"]}`)
old := hold("old", `{"module":"old","version":"1.0","requires":["amqp"]}`)
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Outgrown) != 1 || j.Outgrown[0] != "old" {
t.Fatalf("an outgrown manifest: %+v %v", j, err)
}
@@ -111,8 +111,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if m := module("old"); m.Verdict != "" || len(m.Warnings) != 1 || len(said) != 1 {
t.Fatalf("modules.old carries %+v, said %d", m, len(said))
}
changed := hold("old", `{"module":"old","version":"2","requires":["amqp"]}`)
fresh := hold("new", `{"module":"new","version":"1","requires":["amqp"]}`)
changed := hold("old", `{"module":"old","version":"2.0","requires":["amqp"]}`)
fresh := hold("new", `{"module":"new","version":"1.0","requires":["amqp"]}`)
if j, err := ix.JudgeAll(ctx, map[string]bool{changed: true, fresh: true}); err != nil || len(j.Outgrown) != 0 {
t.Fatalf("a changed and a new manifest: %+v %v", j, err)
}
@@ -142,7 +142,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// manifest a judge refuses in full, its judge pass fails on the verdicts state, and the next pass, which knows nothing
// of that take-in (a start, a pass kicked by a message), still refuses it in full, never as a warning, and ends the key.
ix.Unjudged = newMem()
keptDigest, err := ix.keepManifest(ctx, "kept", `{"module":"kept","version":"1","requires":["amqp"]}`)
keptDigest, err := ix.keepManifest(ctx, "kept", `{"module":"kept","version":"1.0","requires":["amqp"]}`)
if err != nil {
t.Fatal(err)
}
@@ -182,7 +182,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
accepted, ids = append(accepted, a), append(ids, id)
return nil
}
moved := hold("moved", `{"module":"moved","version":"1","requires":[`+
moved := hold("moved", `{"module":"moved","version":"1.0","requires":[`+
`{"name":"cache","storage":"key-value","template":"state","options":{"history":8}},`+
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64}}]}`)
for pass := 0; pass < 2; pass++ {
@@ -208,7 +208,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("said accepted %+v", accepted)
}
// DLV-I9 (design 25 § 12.2): a manifest asking for none, newly accepted, is said too, its entries an empty list.
plain := hold("plain", `{"module":"plain","version":"1"}`)
plain := hold("plain", `{"module":"plain","version":"1.0"}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{plain: true}); err != nil {
t.Fatal(err)
}
@@ -217,8 +217,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
// Review round 1 of issue 1966: a trunk's held manifest judged by its digest (indexer_judge, judgeStale) is said as the
// full pass says it, accepted or refused; a candidate's digest judged over the trunk is said nowhere.
byDigest := hold("bydigest", `{"module":"bydigest","version":"1"}`)
refusedBy := hold("refusedby", `{"module":"refusedby","version":"1","requires":["amqp"]}`)
byDigest := hold("bydigest", `{"module":"bydigest","version":"1.0"}`)
refusedBy := hold("refusedby", `{"module":"refusedby","version":"1.0","requires":["amqp"]}`)
saidBefore := len(said)
for module, digest := range map[string]string{"bydigest": byDigest, "refusedby": refusedBy} {
if _, err := ix.judgeOver(ctx, nil, module, digest); err != nil {
@@ -228,7 +228,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if last := accepted[len(accepted)-1]; last.Digest != byDigest || len(said) != saidBefore+1 || said[len(said)-1].Digest != refusedBy {
t.Fatalf("judged by its digest, the trunk's manifest was said as %+v, refusals %d (were %d)", last, len(said), saidBefore)
}
candidate, err := ix.keepManifest(ctx, "bydigest", `{"module":"bydigest","version":"2"}`)
candidate, err := ix.keepManifest(ctx, "bydigest", `{"module":"bydigest","version":"2.0"}`)
if err != nil {
t.Fatal(err)
}
@@ -239,12 +239,12 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// judging pass says again and clears.
ix.Findings = newMem()
failing = 1
once := hold("once", `{"module":"once","version":"1"}`)
once := hold("once", `{"module":"once","version":"1.0"}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{once: true}); err != nil || accepted[len(accepted)-1].Module != "once" {
t.Fatalf("a publish failing once was not published again: %v %+v", err, accepted[len(accepted)-1])
}
failing = 2
flaky := hold("flaky", `{"module":"flaky","version":"1"}`)
flaky := hold("flaky", `{"module":"flaky","version":"1.0"}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{flaky: true}); err != nil {
t.Fatal(err)
}
@@ -277,13 +277,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
// DLV-I15: a change's renamed-from is judged against the trunk's manifest of the module: a name the trunk declares is
// accepted, one it does not is refused naming the key.
trunkRenamer := hold("renamer", `{"module":"renamer","version":"1","config":{"font-name":{"type":"string","description":"The font.","required":true,"runtime":true}}}`)
trunkRenamer := hold("renamer", `{"module":"renamer","version":"1.0","config":{"font-name":{"type":"string","description":"The font.","required":true,"runtime":true}}}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{trunkRenamer: true}); err != nil {
t.Fatal(err)
}
for raw, want := range map[string]string{
`{"module":"renamer","version":"1","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"font-name"}}}`: "",
`{"module":"renamer","version":"1","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"typeface"}}}`: `renamed from "typeface"`,
`{"module":"renamer","version":"1.0","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"font-name"}}}`: "",
`{"module":"renamer","version":"1.0","config":{"font":{"type":"string","description":"The font.","required":true,"runtime":true,"renamed-from":"typeface"}}}`: `renamed from "typeface"`,
} {
head, err := ix.keepManifest(ctx, "renamer", raw)
if err != nil {
@@ -305,8 +305,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
ix.Packages.(*memStore).put(SeriesKey(sdkGo, "0.1"), value)
}
for raw, refusedAtMerge := range map[string]bool{
`{"module":"pkguser","version":"1","packages":{"@novox/ui":"0.1"}}`: true,
`{"module":"pkguser","version":"2","packages":{"` + sdkGo + `":"0.1"}}`: false,
`{"module":"pkguser","version":"1.0","packages":{"@novox/ui":"0.1"}}`: true,
`{"module":"pkguser","version":"2.0","packages":{"` + sdkGo + `":"0.1"}}`: false,
} {
head, err := ix.keepManifest(ctx, "pkguser", raw)
if err != nil {
@@ -328,8 +328,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// 1966/AC11: modules using state and none using logs but the foundation: logs counts 0, so a manifest of another
// module using logs is refused and the foundation's is accepted.
ix.OldSections = newMem()
hold("user", `{"module":"user","version":"1","state":["x"]}`)
hold("mesh-grants", `{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)
hold("user", `{"module":"user","version":"1.0","state":["x"]}`)
hold("mesh-grants", `{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`)
if _, err := ix.JudgeAll(ctx, nil); err != nil {
t.Fatal(err)
}
@@ -348,7 +348,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("the old sections count %v, want state 1 and the rest 0", c)
}
// DLV-J2, DLV-J3 (ADR 0615 §1): one reads entry on the assigner's state counts 1 for the assigner and 0 for the indexer.
hold("reader", `{"module":"reader","version":"1","state":["x"],"reads":["mesh-delivery-assigner.wants"]}`)
hold("reader", `{"module":"reader","version":"1.0","state":["x"],"reads":["mesh-delivery-assigner.wants"]}`)
if _, err := ix.JudgeAll(ctx, nil); err != nil {
t.Fatal(err)
}
@@ -358,20 +358,24 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("the reads on the owners' state count %v", c.Reads)
}
}
// DLV-J1 (ADR 0619 §1, §3): a foundation part asking bus state in the new form is refused; the nine are the foundation.
cfg := hold("mesh-config", `{"module":"mesh-config","version":"1","requires":[{"name":"cache","storage":"key-value","template":"state"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{cfg: true}); err != nil {
t.Fatal(err)
}
if v := verdict(cfg); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") {
t.Fatalf("mesh-config asking new-form bus state: %+v", v)
// DLV-J1 (ADR 0619 §1, §3): a foundation part asking bus state in the new form is refused; the nine and the pipeline runner are the foundation.
// Design 47 *A module's registration* (b229f5c4 lines 480-481): a template in the manifest of every part on the list is
// refused, the runner's included.
for _, part := range []string{"mesh-config", "mesh-delivery-pipeline"} {
d := hold(part, `{"module":"`+part+`","version":"1.0","requires":[{"name":"cache","storage":"key-value","template":"state"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{d: true}); err != nil {
t.Fatal(err)
}
if v := verdict(d); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") {
t.Fatalf("%s asking new-form bus state: %+v", part, v)
}
}
if !slices.Equal(judge.Foundation, []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"}) {
t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619", judge.Foundation)
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"}) {
t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619 and the pipeline runner (ADR 0666 §4)", judge.Foundation)
}
late := hold("late", `{"module":"late","version":"1","logs":[{"name":"l"}]}`)
grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2","logs":[{"name":"l"},{"name":"m"}]}`)
late := hold("late", `{"module":"late","version":"1.0","logs":[{"name":"l"}]}`)
grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2.0","logs":[{"name":"l"},{"name":"m"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil {
t.Fatal(err)
}
@@ -383,11 +387,11 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
// 1966/AC11: the sole user of an old section changes its manifest and keeps the section: accepted, since it was accepted
// with it (the count is the accepted set held from the last pass, not recounted from fresh digests).
hold("solo", `{"module":"solo","version":"1","objects":[{"name":"o","max-mib":1}]}`)
hold("solo", `{"module":"solo","version":"1.0","objects":[{"name":"o","max-mib":1}]}`)
if _, err := ix.JudgeAll(ctx, nil); err != nil {
t.Fatal(err)
}
solo2 := hold("solo", `{"module":"solo","version":"2","objects":[{"name":"o","max-mib":2}]}`)
solo2 := hold("solo", `{"module":"solo","version":"2.0","objects":[{"name":"o","max-mib":2}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{solo2: true}); err != nil {
t.Fatal(err)
}
@@ -399,7 +403,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
ix.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
held := ix.OldSections
ix.OldSections = unreadable{held}
retry := hold("late", `{"module":"late","version":"2","logs":[{"name":"l"}]}`)
retry := hold("late", `{"module":"late","version":"2.0","logs":[{"name":"l"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{retry: true}); err != nil {
t.Fatal(err)
}
@@ -411,9 +415,9 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's
// build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields.
ownFP := "src2:" + strings.Repeat("e", 64)
hold(Module, `{"module":"mesh-delivery-indexer","version":"1"}`)
hold(Module, `{"module":"mesh-delivery-indexer","version":"1.0"}`)
ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now
later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`)
later := hold("later", `{"module":"later","version":"1.0","a-field-from-later":1,"another":{"x":1}}`)
said = nil
before := len(said)
j, err = ix.JudgeAll(ctx, map[string]bool{later: true})
@@ -446,7 +450,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
ix.ownFP = ownFP
// A renamed field is a known mistake: refused at once.
renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`)
renamed := hold("renamed", `{"module":"renamed","version":"1.0","needs":{"x":"y"}}`)
if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !meshcatalogclient.VerdictRefuses(verdict(renamed)) {
t.Fatalf("a renamed field: %+v %v", j, err)
}
@@ -509,8 +513,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
return digest
}
atStart := stale("tools", `{"module":"tools","version":"7"}`)
ownRefusal := stale("client", `{"module":"client","version":"7"}`)
atStart := stale("tools", `{"module":"tools","version":"7.0"}`)
ownRefusal := stale("client", `{"module":"client","version":"7.0"}`)
if _, _, err := ix.writeVerdict(ctx, ownRefusal, meshcatalog.VerdictsByDigestValue{Module: "client", State: meshcatalogclient.VerdictRefused,
Problems: []string{"x"}, Judge: ix.judgeID(), At: ptr(time.Now())}); err != nil {
t.Fatal(err)
@@ -518,13 +522,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if _, err := ix.JudgeAtStart(ctx); err != nil || meshcatalogclient.VerdictRefuses(verdict(atStart)) || verdict(atStart).Judge != ix.judgeID() || !meshcatalogclient.VerdictRefuses(verdict(ownRefusal)) {
t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err)
}
byPull := stale("tools", `{"module":"tools","version":"8"}`)
byPull := stale("tools", `{"module":"tools","version":"8.0"}`)
pull := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main",
Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}}
Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8.0"}`}}
if _, err := ix.ApplyMessage(ctx, pull); err != nil || meshcatalogclient.VerdictRefuses(verdict(byPull)) || verdict(byPull).Judge != ix.judgeID() {
t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err)
}
byTool := stale("tools", `{"module":"tools","version":"9"}`)
byTool := stale("tools", `{"module":"tools","version":"9.0"}`)
if got := ix.JudgeAgain(ctx, "client", byTool); got.Stored || !strings.Contains(got.Answer, "tools's, not client's") || !meshcatalogclient.VerdictRefuses(verdict(byTool)) {
t.Fatalf("indexer_judge with another module's digest: %+v", got)
}
@@ -17,7 +17,7 @@ func TestBusStateIsJudgedOnce(t *testing.T) {
const ledger = `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"types":[` +
`{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}`
manifest := func(entries string) string {
return `{"module":"a","version":"1","requires":[` + entries + `]}`
return `{"module":"a","version":"1.0","requires":[` + entries + `]}`
}
for _, c := range []struct {
name string
@@ -30,7 +30,7 @@ func TestBusStateIsJudgedOnce(t *testing.T) {
`{"name":"files","storage":"object-store","template":"objects","options":{"max-mib":2048}},` + ledger), nil},
// Design 47: every kept data may say its form and its catch-up tool, in an old section and in `requires` by template,
// and is accepted as read; one saying neither reads as before.
{"a form and a catch-up tool on every kept kind", `{"module":"a","version":"1","tools":["a_take_in"],` +
{"a form and a catch-up tool on every kept kind", `{"module":"a","version":"1.0","tools":["a_take_in"],` +
`"state":["seen",{"name":"modules","form":1,"catch-up":"a_take_in"}],"logs":[{"name":"acts","form":2}],` +
`"streams":[{"name":"forge","events":["merged"],"form":1,"catch-up":"a_take_in"}],` +
`"objects":[{"name":"revisions","max-mib":64,"form":1,"catch-up":"a_take_in"}],"requires":[` +
@@ -40,9 +40,9 @@ func TestBusStateIsJudgedOnce(t *testing.T) {
`{"name":"blobs","storage":"object-store","template":"objects","options":{"max-mib":64},"form":4,"catch-up":"a_take_in"},` +
`{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"form":3,"catch-up":"a_take_in","types":[` +
`{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}]}`, nil},
{"a catch-up tool the module lacks", `{"module":"a","version":"1","tools":["a_read"],"logs":[{"name":"acts","form":1,"catch-up":"a_take_in"}]}`,
{"a catch-up tool the module lacks", `{"module":"a","version":"1.0","tools":["a_read"],"logs":[{"name":"acts","form":1,"catch-up":"a_take_in"}]}`,
[]string{"a's kept log \"acts\"", "catch-up tool \"a_take_in\" is not among the module's tools (a_read)"}},
{"a catch-up tool the module lacks, in requires", `{"module":"a","version":"1","tools":["a_read"],"requires":[` +
{"a catch-up tool the module lacks, in requires", `{"module":"a","version":"1.0","tools":["a_read"],"requires":[` +
`{"name":"holds","storage":"key-value","template":"state","form":1,"catch-up":"nonexistent"}]}`,
[]string{"a's kept state \"holds\"", "catch-up tool \"nonexistent\""}},
{"a form below 0", manifest(`{"name":"x","storage":"key-value","template":"state","form":-1}`), []string{"requires entry \"x\"", "its form is -1"}},
@@ -127,13 +127,13 @@ func TestBusStateIsJudgedOnce(t *testing.T) {
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""},
// DLV-I14 (design 32): the packages section names a known package, by its name in its own language (the packages
// state's names, and a group member's at its head: known), in a well-formed series; through a judge request too.
{"packages naming a known package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""},
{"packages naming a known package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{sdkGo: "0.2"}}}, ""},
{"packages naming a package by the builder's name", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{"mesh-sdk/go": "0.1"}}},
{"packages naming a known package", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "0.2"}}}, ""},
{"packages naming a known package, by request", catalogue.Shelf{"user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "0.2"}}}, ""},
{"packages naming a package by the builder's name", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{"mesh-sdk/go": "0.1"}}},
`user's packages name "mesh-sdk/go", which no package of the mesh is named`},
{"packages naming an unknown package, by request", catalogue.Shelf{"user": {Module: "user", Packages: map[string]string{"@novox/ui": "0.1"}}},
{"packages naming an unknown package, by request", catalogue.Shelf{"user": {Module: "user", Version: "1.0", Packages: map[string]string{"@novox/ui": "0.1"}}},
`user's packages name "@novox/ui", which no package of the mesh is named`},
{"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Packages: map[string]string{sdkGo: "v0.2"}}},
{"packages in an ill-formed series", catalogue.Shelf{"mesh-sdk": sdk, "user": {Module: "user", Version: "1.0", Packages: map[string]string{sdkGo: "v0.2"}}},
`user's packages accept "` + sdkGo + `" in the series "v0.2"`},
} {
got := strings.Join(append(BusStateCatalogueProblems(c.shelf), PackagesCatalogueProblems(c.shelf, known)...), "\n")
@@ -0,0 +1,33 @@
package judge
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"strings"
"testing"
)
// Design 47 G §368, §386, §387, §89: the manifest validator reads the manifests' new words before any manifest uses them.
func TestTheJudgeReadsMilestoneGsWords(t *testing.T) {
for _, row := range []struct {
name string
raw string
want string // a word of the first problem; empty: accepted
}{
{name: "MAJOR.MINOR", raw: `{"module":"a","version":"1.4"}`},
{name: "a pre-release suffix", raw: `{"module":"a","version":"1.4-beta"}`, want: "version"},
{name: "three numbers", raw: `{"module":"a","version":"1.4.2"}`, want: "version"},
{name: "a findings state without its stream", raw: `{"module":"a","version":"1.0","state":["findings"]}`, want: "findings"},
{name: "a findings state with its stream and ask", raw: `{"module":"a","version":"1.0","state":["findings"],` +
`"streams":[{"name":"a-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`},
} {
raws := map[string][]byte{"a": []byte(row.raw)}
var refused map[string][]string
WithSeats([]catalogue.Seat{{Name: "thing", Scope: catalogue.ScopeMesh, Emits: []string{"finding-written"}}}, func() {
refused, _ = Shelf(raws, nil, 0)
})
got := strings.Join(refused["a"], "\n")
if (row.want == "") != (got == "") || !strings.Contains(got, row.want) {
t.Errorf("%s: refused with %q, want %q", row.name, got, row.want)
}
}
}
@@ -0,0 +1,54 @@
package judge
import (
"fmt"
"regexp"
"slices"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The manifests' words milestone G adds, read before any manifest uses them (design 47 G §89: the manifest validator's
// reading of the new words is an edge of kind 4 that the delivery plan derives).
// majorMinor is a manifest's version: two numbers (design 47 G §368). No pre-release or custom suffix exists.
var majorMinor = regexp.MustCompile(`^[0-9]+\.[0-9]+$`)
// FindingsAsked is the event an observer consumes to say its findings again (design 47 G §387).
const FindingsAsked = "mesh-conditions.findings-asked"
// FindingStreamOf is the stream an observer says its findings on (design 47 G §383).
func FindingStreamOf(module string) string { return module + "-findings" }
// versionProblems refuses a manifest whose `version` is not two numbers MAJOR.MINOR, or is missing (design 47 G §368). The
// judge's entry (Manifest) holds a manifest to it; the parser the inputs of a fingerprint are read by does not.
func versionProblems(m catalogue.Manifest) []string {
if majorMinor.MatchString(m.Version) {
return nil
}
return []string{fmt.Sprintf("%s says its version as %q; a manifest names `version` as two numbers MAJOR.MINOR such as 1.4, "+
"and no pre-release or custom suffix exists (design 47 G §368)", m.Module, m.Version)}
}
// gWordsProblems are the problems of a manifest's words milestone G adds: the finding stream and
// ask of a module that declares a state `findings` (§387). The `*` emitter of `consumes` (§386) is judged with the
// other consumed patterns (consumePattern).
func gWordsProblems(m catalogue.Manifest) []string {
var problems []string
for _, s := range m.State {
if s.Name != "findings" {
continue
}
stream := FindingStreamOf(m.Module)
if !slices.ContainsFunc(m.Streams, func(d catalogue.StreamDeclaration) bool { return d.Name == stream }) {
problems = append(problems, fmt.Sprintf("%s declares the state findings without the stream %q its findings are said on "+
"(design 47 G §387)", m.Module, stream))
}
if !slices.Contains(m.Consumes, FindingsAsked) {
problems = append(problems, fmt.Sprintf("%s declares the state findings without consuming %q, with which the "+
"keeper asks its findings again (design 47 G §387)", m.Module, FindingsAsked))
}
break
}
return problems
}
@@ -45,6 +45,9 @@ func Manifest(raw []byte, predefined catalogue.PredefinedList) (m catalogue.Mani
if err != nil {
return catalogue.Manifest{}, problemsOf(err)
}
if problems = versionProblems(m); len(problems) > 0 {
return catalogue.Manifest{}, problems
}
return m, nil
}
@@ -20,27 +20,30 @@ func TestTheJudge(t *testing.T) {
raws map[string]string
want map[string]string // module: a word of its first problem; absent: accepted
}{
{name: "a valid manifest", raws: map[string]string{"a": `{"module":"a","version":"1"}`}},
{name: "a refused manifest", raws: map[string]string{"a": `{"module":"a","version":"1","provides":[{"name":"amqp"}]}`},
{name: "a valid manifest", raws: map[string]string{"a": `{"module":"a","version":"1.0"}`}},
{name: "a refused manifest", raws: map[string]string{"a": `{"module":"a","version":"1.0","provides":[{"name":"amqp"}]}`},
want: map[string]string{"a": `provides "amqp", which is not a provision`}},
{name: "a provision named as a credential's", raws: map[string]string{"a": `{"module":"a","version":"1","provides":[{"name":"secret-db","scope":"mesh"}]}`},
{name: "a provision named as a credential's", raws: map[string]string{"a": `{"module":"a","version":"1.0","provides":[{"name":"secret-db","scope":"mesh"}]}`},
want: map[string]string{"a": `secret-`}},
{name: "an unknown field", raws: map[string]string{"a": `{"module":"a","version":"1","a-field-from-later":1}`},
{name: "an unknown field", raws: map[string]string{"a": `{"module":"a","version":"1.0","a-field-from-later":1}`},
want: map[string]string{"a": `"a-field-from-later"`}},
{name: "a manifest under another name", raws: map[string]string{"b": `{"module":"a","version":"1"}`},
// Design 47 G §23, §28: a step's status is kept in the registration and never in a manifest.
{name: "a manifest naming a step's status", raws: map[string]string{"a": `{"module":"a","version":"1.0","policy":{"lint":"bypass"}}`},
want: map[string]string{"a": `"policy"`}},
{name: "a manifest under another name", raws: map[string]string{"b": `{"module":"a","version":"1.0"}`},
want: map[string]string{"b": `of module "a", not "b"`}},
{name: "a seat declared twice",
raws: map[string]string{
"one": `{"module":"one","version":"1","seats":[{"name":"thing","scope":"mesh"}]}`,
"two": `{"module":"two","version":"1","seats":[{"name":"thing","scope":"mesh"}]}`,
"ok": `{"module":"ok","version":"1"}`},
"one": `{"module":"one","version":"1.0","seats":[{"name":"thing","scope":"mesh"}]}`,
"two": `{"module":"two","version":"1.0","seats":[{"name":"thing","scope":"mesh"}]}`,
"ok": `{"module":"ok","version":"1.0"}`},
want: map[string]string{"one": "already declares", "two": "already declares"}},
// The lead's ruling 5 for A2h: a module that claims a seat and serves its verbs keeps tools of its own beside them
// (the enrolment's first-view tools, DLV-M6), and the judge takes it.
{name: "a claimed seat's module with tools of its own",
raws: map[string]string{
"def": `{"module":"def","version":"1","seats":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}]}`,
"enrolment": `{"module":"enrolment","version":"1","claims":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}],` +
"def": `{"module":"def","version":"1.0","seats":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}]}`,
"enrolment": `{"module":"enrolment","version":"1.0","claims":[{"name":"enrolment","scope":"mesh","serves":["issue","again"]}],` +
`"tools":["enrolment_first_views","enrolment_first_view_again"]}`}},
} {
raws := map[string][]byte{}
@@ -64,10 +67,10 @@ func TestTheJudge(t *testing.T) {
// A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the
// record on a release's Approve); a renamed field and any other problem are refused by both.
waitRaws := map[string][]byte{
"later": []byte(`{"module":"later","version":"1","later-field":true,"other":1}`),
"renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`),
"bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`),
"ok": []byte(`{"module":"ok","version":"1"}`),
"later": []byte(`{"module":"later","version":"1.0","later-field":true,"other":1}`),
"renamed": []byte(`{"module":"renamed","version":"1.0","needs":{"x":"y"}}`),
"bad": []byte(`{"module":"bad","version":"1.0","provides":[{"name":"amqp"}]}`),
"ok": []byte(`{"module":"ok","version":"1.0"}`),
}
refused, waits, _ := ShelfWaiting(waitRaws, nil, 0, nil, nil)
if !reflect.DeepEqual(waits, map[string][]string{"later": {"later-field", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil {
@@ -79,7 +82,7 @@ func TestTheJudge(t *testing.T) {
// The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test).
if raw, err := json.Marshal(overlay.Manifest()); err != nil {
t.Fatal(err)
} else if _, problems := Manifest(raw, nil); len(problems) > 0 {
} else if _, problems := Manifest(versioned(t, raw), nil); len(problems) > 0 {
t.Errorf("the provided private network is refused: %v", problems)
}
shelf := catalogue.Shelf{"nats": {}, "mesh-nats": {}, "a.b": {}}
@@ -100,8 +103,8 @@ func TestTheJudge(t *testing.T) {
// What cmd/mesh-judge answers the checker (ADR 0578 §2): a composer request in, a verdict out, each manifest accepted
// or refused; a manifest left as a digest is a request it cannot run.
r := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "change", Manifests: map[string]composer.Manifest{
"a": {Raw: json.RawMessage(`{"module":"a","version":"1"}`)},
"b": {Raw: json.RawMessage(`{"module":"b","version":"1","requires":["amqp"]}`)},
"a": {Raw: json.RawMessage(`{"module":"a","version":"1.0"}`)},
"b": {Raw: json.RawMessage(`{"module":"b","version":"1.0","requires":["amqp"]}`)},
}}}}
v, err := Request(r)
if err != nil || v.V != composer.Version || len(v.Cases) != 1 || v.SDK != Identity() || !v.Cases[0].Manifests["a"].Accepted ||
@@ -113,13 +116,13 @@ func TestTheJudge(t *testing.T) {
// state, which a trunk module uses, is accepted.
old := composer.Request{V: composer.Version, Cases: []composer.Case{
{Name: "change", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
"late": {Raw: json.RawMessage(`{"module":"late","version":"1","logs":[{"name":"l"}]}`)},
"newer": {Raw: json.RawMessage(`{"module":"newer","version":"1","state":["y"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}},
"user": {Raw: json.RawMessage(`{"module":"user","version":"1.0","state":["x"]}`)},
"late": {Raw: json.RawMessage(`{"module":"late","version":"1.0","logs":[{"name":"l"}]}`)},
"newer": {Raw: json.RawMessage(`{"module":"newer","version":"1.0","state":["y"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`)}}},
{Name: "trunk", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}}}
"user": {Raw: json.RawMessage(`{"module":"user","version":"1.0","state":["x"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1.0","logs":[{"name":"l"}]}`)}}}}}
ov, err := Request(old)
if err != nil {
t.Fatal(err)
@@ -134,3 +137,19 @@ func TestTheJudge(t *testing.T) {
t.Fatal("a manifest left as a digest was judged")
}
}
// versioned is a manifest whose `version` is a MAJOR.MINOR (design 47 G §368): the private network's manifest is
// mesh-networking's, which G's change gives its version.
func versioned(t *testing.T, raw []byte) []byte {
t.Helper()
var fields map[string]any
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatal(err)
}
fields["version"] = "1.0"
out, err := json.Marshal(fields)
if err != nil {
t.Fatal(err)
}
return out
}
@@ -10,7 +10,7 @@ import (
// knowledge is mesh-knowledge's manifest as ADR 0592 has it: an object bucket granted by its trusted value readers (a
// trusted file takes it), and
// two streams that keep their history.
const knowledge = `{"module":"mesh-knowledge","version":"1",
const knowledge = `{"module":"mesh-knowledge","version":"1.0",
"resources":[{"id":"state","type":"directory","mode":"0700","place":"."},
{"id":"readers","trusted":true,"type":"file","path":"${dir:state}/readers.json","mode":"0600",
"merge":"json","takes-settings":["readers"],"content":"{\"readers\": {}}"}],
@@ -41,7 +41,7 @@ func TestObjectBucketsAreJudgedAsADR0592Says(t *testing.T) {
}
}
// A bucket whose name on the bus starts with what the mesh derives names with is refused.
if _, err := ParseManifest([]byte(`{"module":"kv","version":"1","objects":[{"name":"x","max-mib":1}]}`)); err == nil ||
if _, err := ParseManifest([]byte(`{"module":"kv","version":"1.0","objects":[{"name":"x","max-mib":1}]}`)); err == nil ||
!strings.Contains(err.Error(), "KV_") {
t.Errorf("a bucket kv_x was taken: %v", err)
}
@@ -54,7 +54,7 @@ func TestObjectBucketsAreJudgedAsADR0592Says(t *testing.T) {
return m
}
owner := parse(knowledge)
reader := parse(`{"module":"records-page","version":"1","reads":["mesh-knowledge.sections"]}`)
reader := parse(`{"module":"records-page","version":"1.0","reads":["mesh-knowledge.sections"]}`)
shelf := catalogue.Shelf{"mesh-knowledge": owner, "records-page": reader}
if p := objectProblemsOf(CatalogueProblems(shelf)); len(p) > 0 {
t.Fatalf("a granted read was refused: %v", p)
@@ -16,12 +16,13 @@ import (
// OldSections are the sections of a manifest that keep bus state in the old forms.
var OldSections = []string{"state", "logs", "streams", "objects"}
// Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own
// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the
// grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new
// state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused.
// Foundation are the modules that keep using the old sections for good: exactly the ten parts the bus module's own
// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3, as ADR 0666 §4 changes
// them): the bus module, the grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config,
// the vault and the pipeline runner, which runs the bus module's own pipeline. Their new state goes on their old sections
// too, so a foundation manifest asking bus state in the new form is refused.
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"}
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"}
// ViewOwners are the two owners whose state every reader reads through their first views and read tools, never by a
// `reads` entry (novox/hq ADR 0615 §1): CountOwnerReads counts what is left of those entries.
+1 -1
View File
@@ -2536,7 +2536,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
}
m.SetPredefined(predefined)
var problems []string
problems := gWordsProblems(m)
if !name.MatchString(m.Module) {
problems = append(problems, fmt.Sprintf(
"%q is not a usable module name: lower-case letters, digits, dashes and dots", m.Module))
File diff suppressed because it is too large Load Diff
@@ -130,14 +130,14 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
t.Errorf("the indexer names its bucket %s with %d revisions, its manifest's states are %+v", bucket, history, m.State)
}
}
for bucket, read := range map[string]string{index.SourceBucket: "mesh-delivery-gate.source", index.PreviewsBucket: "mesh-delivery-checker.previews",
for bucket, read := range map[string]string{index.PreviewsBucket: "mesh-delivery-checker.previews",
index.ToolchainsBucket(index.BuildGoModule): "mesh-build-go.toolchains", index.ToolchainsBucket(index.BuildNodeModule): "mesh-build-node.toolchains"} {
if !slices.Contains(m.Reads, read) || strings.Replace(read, ".", "_", 1) != bucket {
t.Errorf("the indexer reads the bucket %s, its manifest's reads are %v (want %s)", bucket, m.Reads, read)
}
}
// DLV-F1r (ADR 0615 §1): the assigner's assignments is read through its first view and views family, never its state.
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want)
}
// "Reads FORGE" is a consumes of the indexer's stream: the broker grants only buckets by reads (mesh-control
@@ -145,7 +145,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// The registry seats' published and the builder's numbered fill each package's series (DLV-I5, design 51 *The versions*).
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-builder.version.numbered", "mesh-delivery-checker.preview.written", "mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.started", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**",
"node-engine.manifest-reads-changed", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { // ADR 0632 §2: the counted engines' reads
"mesh-delivery.source-moved", "node-engine.manifest-reads-changed", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { // ADR 0632 §2: the counted engines' reads
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
@@ -157,7 +157,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
// commit is in another (issue 1900).
if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
+3 -2
View File
@@ -69,6 +69,7 @@
"mesh-delivery-checker.try.started",
"mesh-delivery-checker.verdict",
"mesh-delivery-forge-bridge.forge.**",
"mesh-delivery.source-moved",
"node-engine.manifest-reads-changed",
"npm-package-registry.published"
],
@@ -118,8 +119,7 @@
"mesh-build-go.toolchains",
"mesh-build-node.toolchains",
"mesh-delivery-builder.build-log",
"mesh-delivery-checker.previews",
"mesh-delivery-gate.source"
"mesh-delivery-checker.previews"
],
"invokes": [
"mesh-delivery-assigner.assignments",
@@ -131,6 +131,7 @@
"mesh-delivery-forge-bridge.bridge_merged_message",
"seat:node-engine.composition",
"seat:node-engine.manifest-reads",
"seat:mesh-delivery.source",
"seat:mesh-hand-acts.hand-act",
"seat:mesh-watchdogs.presence"
],
@@ -0,0 +1,91 @@
// Command mesh-delivery-pipeline-tools serves the seat mesh-pipeline-runner's verbs as tools named <seat>.<verb>
// (design 47 G §83, §306), answered from the runner's own states; the tool runner describes each from the seat's
// definition, so the tools carry no description or input.
package main
import (
"encoding/json"
"fmt"
"os"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner"
stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-delivery-pipeline/internal/pipeline"
)
// kept reads one of the runner's states through the runtime.
type kept struct{ s stdio.KeptState }
func (k kept) Keys() ([]string, error) { return k.s.Keys() }
func (k kept) Value(key string) (json.RawMessage, error) {
e, err := k.s.Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}
// as decodes a call's arguments into a verb's input, refusing one that does not fit it.
func as[T any](args map[string]any) (T, error) {
var in T
raw, err := json.Marshal(args)
if err == nil {
err = json.Unmarshal(raw, &in)
}
if err != nil {
return in, fmt.Errorf("the arguments do not fit the verb's input: %w", err)
}
return in, nil
}
// Tools are the runner's read verbs over its states pipelines and kept.
func Tools(r pipeline.Reads) []stdio.Tool {
runs := map[string]func(map[string]any) (any, error){
seat.VerbShow: func(a map[string]any) (any, error) {
in, err := as[seat.ShowInput](a)
if err != nil {
return nil, err
}
return r.Show(in)
},
seat.VerbList: func(a map[string]any) (any, error) {
in, err := as[seat.ListInput](a)
if err != nil {
return nil, err
}
return r.List(in)
},
seat.VerbStands: func(map[string]any) (any, error) { return r.Stands() },
seat.VerbWhyCarried: func(a map[string]any) (any, error) {
in, err := as[seat.WhyCarriedInput](a)
if err != nil {
return nil, err
}
return r.WhyCarried(in)
},
seat.VerbKept: func(a map[string]any) (any, error) {
in, err := as[seat.KeptInput](a)
if err != nil {
return nil, err
}
return r.KeptResults(in)
},
}
var out []stdio.Tool
for _, v := range seat.Verbs() {
if run, ok := runs[v]; ok {
out = append(out, stdio.Tool{Name: seat.Seat + "." + v, Run: run})
}
}
return out
}
func main() {
r := pipeline.Reads{Pipelines: kept{stdio.State(seat.StatePipelines)}, Kept: kept{stdio.State(seat.StateKept)}}
if err := stdio.Serve("", Tools(r)); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-pipeline-tools] %v\n", err)
os.Exit(1)
}
}
@@ -0,0 +1,26 @@
package main
import (
"encoding/json"
"strings"
"testing"
"mesh-delivery-pipeline/internal/pipeline"
)
type none struct{}
func (none) Keys() ([]string, error) { return nil, nil }
func (none) Value(string) (json.RawMessage, error) { return nil, nil }
// TestTools: the bundle serves the five read verbs as <seat>.<verb> (§83, §306; RN15/AC1).
func TestTools(t *testing.T) {
var names []string
for _, tl := range Tools(pipeline.Reads{Pipelines: none{}, Kept: none{}}) {
names = append(names, tl.Name)
}
want := "mesh-pipeline-runner.show mesh-pipeline-runner.list mesh-pipeline-runner.stands mesh-pipeline-runner.why-carried mesh-pipeline-runner.kept"
if got := strings.Join(names, " "); got != want {
t.Errorf("tools %q, want %q", got, want)
}
}
@@ -0,0 +1,51 @@
// Command mesh-delivery-pipeline is the pipeline runner, holder of the seat mesh-pipeline-runner, a foundation part
// whose kept data are in its manifest's old sections (design 47 G §21, §27, §101). It names no module, branch or
// repository of its own (§7): its own module is the one its bus account names.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner"
"git.novox.be/novox/mesh-sdk/go/process"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Getenv, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "pipeline runner:", err)
os.Exit(1)
}
}
// run is the program: it returns when ctx ends, or at once when the bus or the state pipelines cannot be used.
func run(ctx context.Context, getenv func(string) string, logOut io.Writer) error {
cred, err := process.ReadBrokerAccount(getenv)
if err != nil {
return err
}
logger := log.New(logOut, cred.Module+": ", log.LstdFlags)
conn, js, err := cred.Dial()
if err != nil {
return err
}
defer conn.Close()
// The state pipelines, `<module>_pipelines`, holds every pipeline by compare-and-set (§27): without it the
// runner keeps nothing, so it stops.
if _, err := process.EssentialBucket(ctx, js, "pipeline runner", ownBucket(cred.Module, seat.StatePipelines), "keep a pipeline"); err != nil {
return err
}
logger.Printf("holding %s", seat.Seat)
<-ctx.Done()
return nil
}
// ownBucket is a state of the runner's old state section on the bus, `<module>_<state>`.
func ownBucket(module, state string) string { return module + "_" + state }
+17
View File
@@ -0,0 +1,17 @@
module mesh-delivery-pipeline
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
github.com/nats-io/nats.go v1.54.0
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+12
View File
@@ -0,0 +1,12 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,191 @@
// Package pipeline is the pipeline runner's own logic, holder of the seat mesh-pipeline-runner (design 47 G §21). It names
// no module, branch or repository (§7): every name comes from what it reads.
package pipeline
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner"
)
// Kept is one of the runner's old state sections as it reads them: every key, and one key's value (nil when absent).
type Kept interface {
Keys() ([]string, error)
Value(key string) (json.RawMessage, error)
}
// Reads answers the runner's read verbs (§83, §306) from its own states pipelines and kept, never another part's.
type Reads struct {
Pipelines Kept
Kept Kept
}
// Show is one pipeline whole, with its steps and the names of their outputs in step-outputs (§83).
func (r Reads) Show(in seat.ShowInput) (seat.ShowAnswer, error) {
p, ok, err := r.pipeline(in.Pipeline)
if err != nil {
return seat.ShowAnswer{}, err
}
if !ok {
return seat.ShowAnswer{}, fmt.Errorf("no pipeline %q in %s", in.Pipeline, seat.StatePipelines)
}
a := seat.ShowAnswer{Pipeline: p}
for _, s := range p.Steps {
if s.Output != "" {
a.Outputs = append(a.Outputs, s.Output)
}
}
return a, nil
}
// List is the pipelines and runs of a module, a branch, a commit or a run, newest started first, at most Limit when it
// names one (§83).
func (r Reads) List(in seat.ListInput) (seat.ListAnswer, error) {
all, err := r.pipelines()
if err != nil {
return seat.ListAnswer{}, err
}
out := []seat.Pipeline{}
for _, p := range all {
if (in.Module == "" || p.Module == in.Module) && (in.Branch == "" || p.Branch == in.Branch) &&
(in.Commit == "" || p.Commit == in.Commit) && (in.Run == "" || p.Run == in.Run) {
out = append(out, p)
}
}
sort.SliceStable(out, func(i, j int) bool { return startedOf(out[i]).After(startedOf(out[j])) })
if in.Limit > 0 && int64(len(out)) > in.Limit {
out = out[:in.Limit]
}
return seat.ListAnswer{Pipelines: out}, nil
}
// Stands is every step that has started and not ended, with what it waits on and since when (§75, §83): the watchman
// seeds its open waits from it.
func (r Reads) Stands() (seat.StandsAnswer, error) {
all, err := r.pipelines()
if err != nil {
return seat.StandsAnswer{}, err
}
out := []seat.Wait{}
for _, p := range all {
for _, s := range p.Steps {
if s.Ended != nil || s.Started == nil {
continue
}
if s.Outcome != seat.StepResultOutcomeRunning && s.Outcome != seat.StepResultOutcomeWaiting {
continue
}
out = append(out, seat.Wait{Pipeline: p.Pipeline, Module: p.Module, Branch: p.Branch, Commit: p.Commit,
Group: p.Group, Run: p.Run, Source: p.Source, Revision: p.Revision, Fp: p.Fp, Step: s.Step,
Artifact: s.Artifact, On: s.On, Since: *s.Started, V: 1})
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].Since.Before(out[j].Since) })
return seat.StandsAnswer{Stands: out}, nil
}
// WhyCarried is where a step's result was carried from (§25, §83): the pipeline it names.
func (r Reads) WhyCarried(in seat.WhyCarriedInput) (seat.WhyCarriedAnswer, error) {
p, ok, err := r.pipeline(in.Pipeline)
if err != nil {
return seat.WhyCarriedAnswer{}, err
}
if !ok {
return seat.WhyCarriedAnswer{}, fmt.Errorf("no pipeline %q in %s", in.Pipeline, seat.StatePipelines)
}
for _, s := range p.Steps {
if s.Step != in.Step || (in.Artifact != "" && s.Artifact != in.Artifact) {
continue
}
if s.Outcome != seat.StepResultOutcomeCarried {
return seat.WhyCarriedAnswer{}, fmt.Errorf("step %s of %s %s was not carried: it ended %q", s.Step,
in.Pipeline, s.Artifact, s.Outcome)
}
return seat.WhyCarriedAnswer{CarriedFrom: s.CarriedFrom}, nil
}
return seat.WhyCarriedAnswer{}, fmt.Errorf("pipeline %q has no step %s %s", in.Pipeline, in.Step, in.Artifact)
}
// KeptResults is the kept passing results, by level, digest or package (§83).
func (r Reads) KeptResults(in seat.KeptInput) (seat.KeptAnswer, error) {
keys, err := r.Kept.Keys()
if err != nil {
return seat.KeptAnswer{}, fmt.Errorf("%s cannot be read: %w", seat.StateKept, err)
}
sort.Strings(keys)
out := []seat.KeptResult{}
for _, k := range keys {
level, digest, ok := seat.ParseKeptByLevelDigestKey(k)
if !ok || (in.Level != "" && level != in.Level) || (in.Digest != "" && digest != in.Digest) {
continue
}
raw, err := r.Kept.Value(k)
if err != nil {
return seat.KeptAnswer{}, fmt.Errorf("%s %s cannot be read: %w", seat.StateKept, k, err)
}
if raw == nil {
continue
}
v, err := seat.DecodeKeptByLevelDigestValue(raw)
if err != nil {
return seat.KeptAnswer{}, fmt.Errorf("%s %s: %w", seat.StateKept, k, err)
}
if in.Package != "" && v.Package != in.Package {
continue
}
out = append(out, v)
}
return seat.KeptAnswer{Kept: out}, nil
}
func (r Reads) pipeline(key string) (seat.Pipeline, bool, error) {
key = strings.TrimSpace(key)
if key == "" {
return seat.Pipeline{}, false, fmt.Errorf("name the pipeline's key, <branch>.<revision>.<module>")
}
raw, err := r.Pipelines.Value(key)
if err != nil {
return seat.Pipeline{}, false, fmt.Errorf("%s %s cannot be read: %w", seat.StatePipelines, key, err)
}
if raw == nil {
return seat.Pipeline{}, false, nil
}
p, err := seat.DecodePipelinesByBranchRevisionModuleValue(raw)
if err != nil {
return seat.Pipeline{}, false, fmt.Errorf("%s %s: %w", seat.StatePipelines, key, err)
}
if p.Pipeline == "" {
p.Pipeline = key
}
return p, true, nil
}
func (r Reads) pipelines() ([]seat.Pipeline, error) {
keys, err := r.Pipelines.Keys()
if err != nil {
return nil, fmt.Errorf("%s cannot be read: %w", seat.StatePipelines, err)
}
sort.Strings(keys)
var out []seat.Pipeline
for _, k := range keys {
p, ok, err := r.pipeline(k)
if err != nil {
return nil, err
}
if ok {
out = append(out, p)
}
}
return out, nil
}
func startedOf(p seat.Pipeline) time.Time {
if p.Started == nil {
return time.Time{}
}
return *p.Started
}
@@ -0,0 +1,129 @@
package pipeline
import (
"encoding/json"
"strings"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner"
)
type memKept map[string]json.RawMessage
func (m memKept) Keys() ([]string, error) {
var out []string
for k := range m {
out = append(out, k)
}
return out, nil
}
func (m memKept) Value(key string) (json.RawMessage, error) { return m[key], nil }
func put(t *testing.T, m memKept, key string, v any) {
t.Helper()
raw, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
m[key] = raw
}
// TestReads holds the runner's read verbs to §83 and §306, one row per case (RN15/AC1, test 93's runner case).
func TestReads(t *testing.T) {
t0 := time.Date(2026, 11, 11, 10, 0, 0, 0, time.UTC)
t1, t2 := t0.Add(time.Minute), t0.Add(2*time.Minute)
pipes, kept := memKept{}, memKept{}
put(t, pipes, "main.7.alpha", seat.Pipeline{Pipeline: "main.7.alpha", Module: "alpha", Branch: "main", Commit: "c7",
Source: 7, Outcome: seat.PipelineOutcomeRunning, Started: &t0, Steps: []seat.StepResult{
{Step: seat.StepResultStepLint, Artifact: "alpha:program", Status: "required", Outcome: seat.StepResultOutcomePassed,
Started: &t0, Ended: &t1, Output: "main.7.alpha.lint.alpha:program"},
{Step: seat.StepResultStepUnit, Artifact: "alpha:program", Status: "required", Outcome: seat.StepResultOutcomeCarried,
CarriedFrom: "main.6.alpha"},
{Step: seat.StepResultStepApproval, Status: "required", Outcome: seat.StepResultOutcomeWaiting, On: seat.WaitOnPerson,
Started: &t1},
}})
put(t, pipes, "beta.3.alpha", seat.Pipeline{Pipeline: "beta.3.alpha", Module: "alpha", Branch: "beta", Commit: "b3",
Outcome: seat.PipelineOutcomePassed, Started: &t2})
put(t, pipes, "main.7.gamma", seat.Pipeline{Pipeline: "main.7.gamma", Module: "gamma", Branch: "main", Commit: "c7",
Outcome: seat.PipelineOutcomeRunning, Started: &t1})
put(t, kept, "unit.sha256-aa", seat.KeptResult{Level: "unit", Digest: "sha256-aa", Package: "alpha/x"})
put(t, kept, "integration.sha256-bb", seat.KeptResult{Level: "integration", Digest: "sha256-bb", Package: "alpha/y"})
r := Reads{Pipelines: pipes, Kept: kept}
names := func(ps []seat.Pipeline) string {
var s []string
for _, p := range ps {
s = append(s, p.Pipeline)
}
return strings.Join(s, " ")
}
rows := []struct {
name string
got func() (string, error)
want string
}{
{"show answers the pipeline whole and its outputs", func() (string, error) {
a, err := r.Show(seat.ShowInput{Pipeline: "main.7.alpha"})
return a.Pipeline.Module + " " + strings.Join(a.Outputs, ","), err
}, "alpha main.7.alpha.lint.alpha:program"},
{"show refuses a pipeline it does not hold", func() (string, error) {
_, err := r.Show(seat.ShowInput{Pipeline: "main.9.alpha"})
return "", err
}, `error: no pipeline "main.9.alpha" in pipelines`},
{"list by module, newest first", func() (string, error) {
a, err := r.List(seat.ListInput{Module: "alpha"})
return names(a.Pipelines), err
}, "beta.3.alpha main.7.alpha"},
{"list by commit", func() (string, error) {
a, err := r.List(seat.ListInput{Commit: "c7"})
return names(a.Pipelines), err
}, "main.7.gamma main.7.alpha"},
{"list by branch with a limit", func() (string, error) {
a, err := r.List(seat.ListInput{Branch: "main", Limit: 1})
return names(a.Pipelines), err
}, "main.7.gamma"},
{"stands names a waiting step and since when", func() (string, error) {
a, err := r.Stands()
var s []string
for _, w := range a.Stands {
s = append(s, w.Pipeline+" "+w.Step+" "+w.On+" "+w.Since.Format("15:04"))
}
return strings.Join(s, ";"), err
}, "main.7.alpha approval person 10:01"},
{"why-carried names where a carried step came from", func() (string, error) {
a, err := r.WhyCarried(seat.WhyCarriedInput{Pipeline: "main.7.alpha", Step: "unit", Artifact: "alpha:program"})
return a.CarriedFrom, err
}, "main.6.alpha"},
{"why-carried refuses a step that ran", func() (string, error) {
_, err := r.WhyCarried(seat.WhyCarriedInput{Pipeline: "main.7.alpha", Step: "lint"})
return "", err
}, `error: step lint of main.7.alpha alpha:program was not carried: it ended "passed"`},
{"kept by level", func() (string, error) {
a, err := r.KeptResults(seat.KeptInput{Level: "unit"})
var s []string
for _, k := range a.Kept {
s = append(s, k.Package)
}
return strings.Join(s, " "), err
}, "alpha/x"},
{"kept by package", func() (string, error) {
a, err := r.KeptResults(seat.KeptInput{Package: "alpha/y"})
var s []string
for _, k := range a.Kept {
s = append(s, k.Digest)
}
return strings.Join(s, " "), err
}, "sha256-bb"},
}
for _, row := range rows {
got, err := row.got()
if err != nil {
got = "error: " + err.Error()
}
if got != row.want {
t.Errorf("%s: got %q, want %q", row.name, got, row.want)
}
}
}
@@ -0,0 +1,77 @@
package pipeline_test
import (
"os"
"slices"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// TestManifest holds the runner's manifest to its design lines, row by row.
func TestManifest(t *testing.T) {
raw, err := os.ReadFile("module.json")
if err != nil {
t.Fatal(err)
}
m, err := catalogue.Decode(raw)
if err != nil {
t.Fatalf("the manifest does not read: %v", err)
}
var states, streams, objects []string
for _, s := range m.State {
states = append(states, s.Name)
}
for _, s := range m.Streams {
streams = append(streams, s.Name)
}
for _, o := range m.Objects {
objects = append(objects, o.Name)
}
var program catalogue.Artifact
if m.Build == nil {
t.Fatal("the manifest builds nothing")
}
for _, a := range m.Build.Artifacts {
if a.Name == "program" {
program = a
}
}
rows := []struct {
name string
ok bool
}{
// RN1/AC1 (§21): a foundation part asks no template; its kept data are in old sections.
{"no requires", len(m.Requires) == 0},
{"claims the seat serving every verb of its definition", len(m.Claims) == 1 && m.Claims[0].Name == seat.Seat &&
equalSets(m.Claims[0].Serves, seat.Verbs())},
// §27, §85, §330, §355, §77: the old state sections.
{"old state sections", equalSets(states, []string{seat.StatePipelines, seat.StateKept, "aged", "findings", "first-views"})},
// §27, §85: the step log on an old stream, its outputs in an old objects section.
{"old streams section", equalSets(streams, []string{seat.StateStepLog})},
{"step-log 128 MiB", len(m.Streams) == 1 && m.Streams[0].MaxMiB == 128},
{"old objects section", equalSets(objects, []string{seat.StateStepOutputs})},
{"step-outputs 1 GiB", len(m.Objects) == 1 && m.Objects[0].MaxMiB == 1024},
// §333, §54, §44, §53, §376, §24, §37, §4, §77: what the runner hears.
{"consumes", equalSets(m.Consumes, []string{
"mesh-delivery-indexer.take-in.taken", "mesh-builds.package-published", "mesh-builds.toolchain-agreed",
"operator-channel.decided", "node-build-go.step-ended", "node-build-node.step-ended",
"node-build-image.step-ended", "mesh-wants.step-ended", "mesh-catalog.registration-changed",
"mesh-catalog.catalog-revision"})},
// RN1/AC5 (§95, §130): the new assigner is delivered before the runner.
{"program needs the assigner's program", slices.Equal(program.Needs, []string{"mesh-delivery-assigner:program"})},
}
for _, r := range rows {
if !r.ok {
t.Errorf("%s: not as the design says", r.name)
}
}
}
func equalSets(a, b []string) bool {
x, y := slices.Clone(a), slices.Clone(b)
slices.Sort(x)
slices.Sort(y)
return slices.Equal(x, y)
}
+137
View File
@@ -0,0 +1,137 @@
{
"module": "mesh-delivery-pipeline",
"version": "1",
"slug": "pipeline",
"claims": [
{
"name": "mesh-pipeline-runner",
"scope": "mesh",
"serves": [
"run",
"start",
"show",
"list",
"stands",
"why-carried",
"kept",
"review",
"rerun",
"accept",
"cancel",
"ask-again"
]
}
],
"consumes": [
"mesh-delivery-indexer.take-in.taken",
"mesh-builds.package-published",
"mesh-builds.toolchain-agreed",
"operator-channel.decided",
"node-build-go.step-ended",
"node-build-node.step-ended",
"node-build-image.step-ended",
"mesh-wants.step-ended",
"mesh-catalog.registration-changed",
"mesh-catalog.catalog-revision"
],
"state": [
{
"name": "pipelines",
"history": 1
},
{
"name": "kept",
"history": 1
},
{
"name": "aged",
"history": 1
},
{
"name": "findings",
"history": 1
},
{
"name": "first-views",
"history": 1
}
],
"streams": [
{
"name": "step-log",
"events": [
"step-log.**"
],
"max-mib": 128
}
],
"objects": [
{
"name": "step-outputs",
"max-mib": 1024
}
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-delivery-pipeline",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-delivery-pipeline",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-delivery-pipeline"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-delivery-pipeline",
"place": ".",
"unknown": "program"
},
{
"id": "pipeline",
"type": "process",
"name": "mesh-delivery-pipeline",
"artifact": "program",
"run": [
"./mesh-delivery-pipeline"
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker"
},
"user": "mesh-delivery-pipeline",
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "program",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-pipeline",
"binary": "mesh-delivery-pipeline",
"needs": [
"mesh-delivery-assigner:program"
]
},
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-pipeline-tools",
"binary": "mesh-delivery-pipeline-tools",
"loads": [
"mesh-delivery-pipeline-tools"
]
}
]
}
}