A-163: the asker in `accept.<queue>.<asker>` is the asking seat's name (mesh-pipeline-runner, mesh-catalog, mesh-wants, mesh-builds), never a nickname: the builder asks as mesh-builds (was "builder"), the indexer's validation as mesh-catalog (was "indexer"), the assigner's runs as mesh-wants (was "assigner"), the runner's steps on `accept.check.mesh-pipeline-runner` and `accept.delivery.mesh-pipeline-runner` (were `accept.check.step` and `accept.delivery.step`, where the holder read `step` as the asker). An asker naming none (the checker) asks on main's subject (A-184). A build seat's queues are delivery, check and recheck alone (A-170, A-184): buildstep's AskKinds holds no step, a holder takes a step from check or delivery, and the grants part makes a worker per queue, never per kind. A holder keys a named ask's step `<asker>.<node>.<id>` and main's `<node>.<id>` (§481, §483); readers watch their own name's keys. The composition step's step-progress names the one caller mesh-wants' definition lists for step, read from the seat list (A-294). Builds: design 47 G §481, §483, §543, §600, A-163, A-170, A-184, A-248, A-294 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
648 lines
30 KiB
Go
648 lines
30 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
const (
|
|
testCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
testAskID = "m2kx9a1b3c4d5e"
|
|
)
|
|
|
|
func goodBuild() StepAsk {
|
|
return StepAsk{ID: testAskID, Kind: AskDelivery, Language: AskGo, Asker: "mesh-delivery-builder",
|
|
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
|
Build: &BuildJob{Module: "nats", Path: "modules/nats", Fetch: &Fetch{Repository: "novox/mesh-catalog", Commit: testCommit},
|
|
Artifacts: []AskArtifact{{Artifact: "go"}}}}
|
|
}
|
|
|
|
func goodCheck() StepAsk {
|
|
return StepAsk{ID: testAskID, Kind: AskCheck, Language: AskGo, Asker: "mesh-delivery-checker",
|
|
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
|
Check: &CheckJob{Fetch: Fetch{Repository: "novox/mesh-catalog", Commit: testCommit}, Levels: []string{LevelUnit}}}
|
|
}
|
|
|
|
// runAsk is the checker's run of a checking program built at a mesh-delivery head, over two heads (issue 1921, B0).
|
|
func runAsk() StepAsk {
|
|
in, err := RunInputOf("seat:mesh-delivery-checker.input", []byte(`{"v":1}`))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return StepAsk{ID: testAskID, Kind: AskCheck, Language: AskGo, Asker: "mesh-delivery-checker",
|
|
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
|
Run: &RunJob{Program: RunProgram{Repository: "novox/mesh-delivery", Commit: testCommit, Dir: "modules/mesh-delivery-checker",
|
|
Package: "./cmd/mesh-checking-program"},
|
|
Heads: []RunHead{{Repository: "novox/mesh-delivery", Commit: testCommit}, {Repository: "novox/mesh-sdk", Commit: testCommit}},
|
|
Input: in, Args: []string{"-verdict"}}}
|
|
}
|
|
|
|
// stepAsk is the runner's unit step of one artifact (086 A-D31): its kept result.
|
|
func stepAsk() StepAsk {
|
|
return StepAsk{ID: testAskID, Kind: AskStep, Language: AskGo, Asker: "mesh-delivery-pipeline",
|
|
At: time.Date(2026, 11, 10, 10, 0, 0, 0, time.UTC),
|
|
Step: &StepRun{Pipeline: "main.12.nats", Step: StepUnit, Module: "nats", Artifact: "nats:program", Branch: "main",
|
|
Commit: testCommit, Revision: 12, Fingerprint: "fp-0123", Fetch: Fetch{Repository: "novox/mesh-catalog", Commit: testCommit},
|
|
Kept: []KeptResult{{Package: "./modules/nats/a", Digest: "sha256:bbbb", Level: StepUnit}}}}
|
|
}
|
|
|
|
// The ask the three build seats share is one neutral form: it round-trips, and every ill-formed ask is refused,
|
|
// naming what is wrong (the rules of the old delivery.StepAsk, now the shared runner's).
|
|
func TestAskCheckAndRoundTrip(t *testing.T) {
|
|
headless := runAsk() // a run whose program reads only its input names no head (design 47 G §712, A-238)
|
|
headless.Run.Heads = nil
|
|
for _, a := range []StepAsk{goodBuild(), goodCheck(), runAsk(), headless, stepAsk()} {
|
|
raw, err := EncodeStepAsk(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeStepAsk(raw)
|
|
if err != nil || back.ID != a.ID || back.V != StepAskVersion {
|
|
t.Fatalf("round trip: %+v, %v", back, err)
|
|
}
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
edit func(*StepAsk)
|
|
want string
|
|
}{
|
|
{"no id", func(a *StepAsk) { a.ID = "x" }, "not an ask id"},
|
|
{"unknown kind", func(a *StepAsk) { a.Kind = "build" }, "ask kind"},
|
|
{"unknown language", func(a *StepAsk) { a.Language = "rust" }, "ask language"},
|
|
{"no time", func(a *StepAsk) { a.At = time.Time{} }, "names no time"},
|
|
{"recheck build", func(a *StepAsk) { a.Kind = AskRecheck }, "recheck stream"},
|
|
{"two jobs", func(a *StepAsk) { a.Check = goodCheck().Check }, "exactly one"},
|
|
{"needs on a build", func(a *StepAsk) { a.Needs = []string{NeedNodeBuildNode} }, "names needs"},
|
|
{"unknown registry seat", func(a *StepAsk) { a.Build.Artifacts[0].Seat = "somewhere" }, "no registry seat"},
|
|
{"version without seat", func(a *StepAsk) { a.Build.Artifacts[0].Version = "1.0.0" }, "names no seat"},
|
|
{"no path", func(a *StepAsk) { a.Build.Path = "" }, "names no directory"},
|
|
{"short commit", func(a *StepAsk) { a.Build.Fetch.Commit = "abc" }, "full commit"},
|
|
{"bad package pin", func(a *StepAsk) { a.Packages = PackageVersions{"m": "main"} }, "neither a full commit"},
|
|
} {
|
|
a := goodBuild()
|
|
c.edit(&a)
|
|
if _, err := EncodeStepAsk(a); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
|
}
|
|
}
|
|
// 1921 (B0, G): a run ask goes only on the Go seat's check stream, alone, builds a clean package at a full commit
|
|
// over heads named once at full commits, and names its input on a seat's verb by a digest within its bound.
|
|
for _, c := range []struct {
|
|
name string
|
|
edit func(*StepAsk)
|
|
want string
|
|
}{
|
|
{"run beside a check", func(a *StepAsk) { a.Check = goodCheck().Check }, "or a step, exactly one"},
|
|
{"run on the delivery stream", func(a *StepAsk) { a.Kind = AskDelivery }, "is a run"},
|
|
{"run on the Node seat", func(a *StepAsk) { a.Language = AskNode }, "is a run"},
|
|
{"run at a short commit", func(a *StepAsk) { a.Run.Program.Commit = "0123456" }, "no full commit"},
|
|
{"run outside the repository", func(a *StepAsk) { a.Run.Program.Dir = "../x" }, "no clean directory"},
|
|
{"run of no package", func(a *StepAsk) { a.Run.Program.Package = "" }, "no package"},
|
|
{"run over a head twice", func(a *StepAsk) { a.Run.Heads = append(a.Run.Heads, a.Run.Heads[0]) }, "twice"},
|
|
{"run over a branch", func(a *StepAsk) { a.Run.Heads[1].Commit = "main" }, "novox/mesh-sdk at no full commit"},
|
|
{"input on a state", func(a *StepAsk) { a.Run.Input.Verb = "mesh-delivery-checker.inputs" }, "no seat:<seat>.<verb>"},
|
|
{"input on one machine", func(a *StepAsk) { a.Run.Input.Verb = "seat:mesh-delivery-checker.input@ace" }, "no seat:<seat>.<verb>"},
|
|
{"input by no digest", func(a *StepAsk) { a.Run.Input.Digest = "d1" }, "no sha256 digest"},
|
|
{"input of no bytes", func(a *StepAsk) { a.Run.Input.Size = 0 }, "under 1"},
|
|
{"argument of the shell", func(a *StepAsk) { a.Run.Args = []string{"-x; rm"} }, "no word or flag"},
|
|
} {
|
|
a := runAsk()
|
|
c.edit(&a)
|
|
if _, err := EncodeStepAsk(a); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
|
}
|
|
}
|
|
// 086 A-D31: a pipeline step is the accepted kind step alone, names its artifact of its module at a full commit and
|
|
// fingerprint, and carries the registrations' names; a rerun names the caller the runtime proved.
|
|
for _, c := range []struct {
|
|
name string
|
|
edit func(*StepAsk)
|
|
want string
|
|
}{
|
|
{"step beside a check", func(a *StepAsk) { a.Check = goodCheck().Check }, "exactly one"},
|
|
{"step sent as a check", func(a *StepAsk) { a.Kind = AskCheck }, "carries a step alone"},
|
|
{"check sent as a step", func(a *StepAsk) { a.Step, a.Check = nil, goodCheck().Check }, "carries a step alone"},
|
|
{"unit step of an image", func(a *StepAsk) { a.Language = AskImage }, "has no tests"},
|
|
{"unknown step", func(a *StepAsk) { a.Step.Step = "review" }, `the step "review"`},
|
|
{"artifact of another module", func(a *StepAsk) { a.Step.Artifact = "nats-tools:program" }, "not nats:<artifact>"},
|
|
{"step at a branch", func(a *StepAsk) { a.Step.Commit = "main" }, "no full commit"},
|
|
{"no fingerprint", func(a *StepAsk) { a.Step.Fingerprint = "" }, "no build-input fingerprint"},
|
|
{"rerun with no caller", func(a *StepAsk) { a.Step.Rerun = true }, "rerun with no caller"},
|
|
} {
|
|
a := stepAsk()
|
|
c.edit(&a)
|
|
if _, err := EncodeStepAsk(a); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
|
}
|
|
}
|
|
if a := stepAsk(); !a.Step.Skips("./modules/nats/a", "sha256:bbbb") || a.Step.Skips("./modules/nats/a", "sha256:cccc") {
|
|
t.Error("a kept result skips its package at its digest and level only")
|
|
}
|
|
// A-237: a run's input is any size.
|
|
if in, err := RunInputOf("seat:mesh-delivery-checker.input", make([]byte, 3<<20)); err != nil || in.Size != 3<<20 {
|
|
t.Errorf("an input of 3 MiB: %+v, %v", in, err)
|
|
}
|
|
in := runAsk().Run.Input
|
|
if err := in.Verify([]byte(`{"v":1}`)); err != nil {
|
|
t.Errorf("the input's own bytes: %v", err)
|
|
}
|
|
if err := in.Verify([]byte(`{"v":2}`)); err == nil || !strings.Contains(err.Error(), "not the one the ask names") {
|
|
t.Errorf("bytes that are not the digest: %v", err)
|
|
}
|
|
chk := goodCheck()
|
|
chk.Check.Levels = []string{"nightly"}
|
|
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "nightly") {
|
|
t.Errorf("a level: %v", err)
|
|
}
|
|
chk = goodCheck()
|
|
chk.Check.Toolchain = &ToolchainUsed{Language: "go", Version: "1.26.0", Recipe: "2"}
|
|
raw, err := EncodeStepAsk(chk)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back, err := DecodeStepAsk(raw); err != nil || back.Check.Toolchain == nil || back.Check.Toolchain.Version != "1.26.0" {
|
|
t.Errorf("the check's toolchain is lost in a round trip: %+v, %v", back.Check, err)
|
|
}
|
|
chk.Check.Toolchain.Recipe = ""
|
|
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "toolchain") {
|
|
t.Errorf("a toolchain without a recipe: %v", err)
|
|
}
|
|
chk = goodCheck()
|
|
chk.Language = AskImage
|
|
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "no tests") {
|
|
t.Errorf("an image check: %v", err)
|
|
}
|
|
if _, err := DecodeStepAsk([]byte(`{"v":2}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("another version: %v", err)
|
|
}
|
|
// Issue 1966: a compose job's request travels packed when it passes the bus's message limit, and reads back as sent.
|
|
t.Run("1966: a compose request travels packed", func(t *testing.T) {
|
|
obj := []byte(`{"v":1,"input":{"nodes":[]}}`)
|
|
job := ComposeJob{Fetch: Fetch{Repository: "novox/mesh-host", Commit: strings.Repeat("a", 40)}, Dir: ".", Package: "p"}
|
|
if err := job.PackRequest(obj); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := job.RequestBytes(); err != nil || string(got) != string(obj) {
|
|
t.Errorf("read back %s, %v", got, err)
|
|
}
|
|
if err := job.check("ask"); err != nil {
|
|
t.Errorf("a packed request is refused: %v", err)
|
|
}
|
|
job.Request = json.RawMessage(`"not packed"`)
|
|
if err := job.check("ask"); err == nil {
|
|
t.Errorf("a string that is no packed request is accepted")
|
|
}
|
|
job.Request = obj
|
|
if got, err := job.RequestBytes(); err != nil || string(got) != string(obj) {
|
|
t.Errorf("a whole request read back %s, %v", got, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The seat's value and the neutral form are the same JSON: AskOf takes any seat's type (here its shape, with a
|
|
// pointer time), AskAs gives one back.
|
|
func TestAskOfAnySeatsType(t *testing.T) {
|
|
type seatAsk struct {
|
|
V int64 `json:"v,omitempty"`
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Lang string `json:"language"`
|
|
Asker string `json:"asker"`
|
|
At *time.Time `json:"at,omitempty"`
|
|
Check *struct {
|
|
Fetch Fetch `json:"fetch"`
|
|
Levels []string `json:"levels"`
|
|
} `json:"check,omitempty"`
|
|
}
|
|
at := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
|
|
s := seatAsk{V: 1, ID: testAskID, Kind: AskCheck, Lang: AskGo, Asker: "mesh-delivery-checker", At: &at}
|
|
s.Check = &struct {
|
|
Fetch Fetch `json:"fetch"`
|
|
Levels []string `json:"levels"`
|
|
}{Fetch{Repository: "novox/mesh-catalog", Commit: testCommit}, []string{LevelUnit}}
|
|
a, err := AskOf(s)
|
|
if err != nil || a.Check == nil || !a.At.Equal(at) || a.Check.Fetch.Commit != testCommit {
|
|
t.Fatalf("%+v %v", a, err)
|
|
}
|
|
back, err := AskAs[seatAsk](a)
|
|
if err != nil || back.ID != s.ID || !back.At.Equal(at) {
|
|
t.Fatalf("%+v %v", back, err)
|
|
}
|
|
s.Check = nil
|
|
if _, err := AskOf(s); err == nil {
|
|
t.Error("an ask with no job is accepted")
|
|
}
|
|
}
|
|
|
|
func TestAskSubjectsStreamsAndKeys(t *testing.T) {
|
|
for _, l := range AskLanguages {
|
|
for _, k := range AskKinds {
|
|
subj, err := AskSubject(l, k)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gl, gk, err := ParseAskSubject(subj)
|
|
if err != nil || gl != l || gk != k {
|
|
t.Errorf("%s: %s %s %v", subj, gl, gk, err)
|
|
}
|
|
}
|
|
}
|
|
if got, _ := AskWorker(AskGo, AskCheck); got != "SEAT_NODE_BUILD_GO_CHECK_worker" {
|
|
t.Errorf("worker %q", got)
|
|
}
|
|
if got, _ := AskStream(AskImage); got != "SEAT_NODE_BUILD_IMAGE" {
|
|
t.Errorf("stream %q", got)
|
|
}
|
|
if _, _, err := ParseAskSubject("mesh.seat.node-build-go.accept.nothing"); err == nil {
|
|
t.Error("an unknown kind parses")
|
|
}
|
|
if _, err := AskSubject("rust", AskCheck); err == nil {
|
|
t.Error("an unknown language has a subject")
|
|
}
|
|
// §481, §483: a named ask's step is keyed under its asker, main's under its node alone.
|
|
for _, c := range []struct{ asker, key, watch string }{
|
|
{"", "ace." + testAskID, "*." + testAskID},
|
|
{"mesh-pipeline-runner", "mesh-pipeline-runner.ace." + testAskID, "mesh-pipeline-runner.*." + testAskID},
|
|
} {
|
|
if k, err := AskStepKey(c.asker, "ace", testAskID); err != nil || k != c.key {
|
|
t.Errorf("%q: key %q %v", c.asker, k, err)
|
|
}
|
|
if w, _ := AskStepWatch(c.asker, testAskID); w != c.watch {
|
|
t.Errorf("%q: watch %q", c.asker, w)
|
|
}
|
|
if a, n, id, err := ParseStepKey(c.key); err != nil || a != c.asker || n != "ace" || id != testAskID {
|
|
t.Errorf("%q: parsed %q %q %q %v", c.key, a, n, id, err)
|
|
}
|
|
}
|
|
if _, err := AskStepKey("Bad.Name", "ace", testAskID); err == nil {
|
|
t.Error("a bad asker has a step key")
|
|
}
|
|
if _, err := AskSubject(AskGo, AskStep); err == nil {
|
|
t.Error("step is a queue: it rides check or delivery, its kind in the body (A-170, A-184)")
|
|
}
|
|
if k, _ := PauseKey("ace"); k != "paused.ace" {
|
|
t.Errorf("pause key %q", k)
|
|
}
|
|
if k, _ := CapacityKey("ace"); k != "capacity.ace" {
|
|
t.Errorf("capacity key %q", k)
|
|
}
|
|
if _, err := CapacityKey("a.b"); err == nil {
|
|
t.Error("a dotted node keys")
|
|
}
|
|
// 086 A-D31: a step's end is said on its own node's subject, and one naming another node is refused.
|
|
subj, err := StepEndedSubject(AskNode, "ace")
|
|
if err != nil || subj != "mesh.seat.node-build-node.event.step-ended.ace" {
|
|
t.Fatalf("step-ended subject %q %v", subj, err)
|
|
}
|
|
if l, n, err := ParseStepEndedSubject(subj); err != nil || l != AskNode || n != "ace" {
|
|
t.Errorf("parse %q: %s %s %v", subj, l, n, err)
|
|
}
|
|
end, err := EndOf(stepAsk(), "ace", OutcomePassed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
end.Tests = &TestCounts{Unit: 14}
|
|
raw, err := EncodeStepEnded(end)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
goSubj, _ := StepEndedSubject(AskGo, "ace")
|
|
if back, err := DecodeStepEnded(goSubj, raw); err != nil || back.Fingerprint != "fp-0123" || back.Tests.Unit != 14 ||
|
|
back.Asker != stepAsk().Asker || back.Ask != testAskID {
|
|
t.Errorf("round trip: %+v %v", back, err)
|
|
}
|
|
// 1966/CT12 (G §548, §602): the end of an ask outside a pipeline names its asker, the ask and when, never a commit.
|
|
at := time.Date(2026, 11, 11, 10, 3, 0, 0, time.UTC)
|
|
build := StepEnded{ID: testAskID, Asker: "mesh-delivery-builder", Ask: testAskID, Node: "ace", Outcome: OutcomePassed, At: at}
|
|
if raw, err := EncodeStepEnded(build); err != nil || strings.Contains(string(raw), "commit") {
|
|
t.Errorf("a builder's build end: %s %v", raw, err)
|
|
} else if back, err := DecodeStepEnded(goSubj, raw); err != nil || back.Asker != "mesh-delivery-builder" || !back.At.Equal(at) {
|
|
t.Errorf("a builder's build end reads back as %+v %v", back, err)
|
|
}
|
|
for name, bad := range map[string]func(*StepEnded){
|
|
"no asker": func(e *StepEnded) { e.Asker = "" },
|
|
"no time": func(e *StepEnded) { e.At = time.Time{} },
|
|
"a commit": func(e *StepEnded) { e.Commit = "0123456789abcdef0123456789abcdef01234567" },
|
|
"a fingerprint": func(e *StepEnded) { e.Fingerprint = "fp-0123" },
|
|
} {
|
|
e := build
|
|
bad(&e)
|
|
if _, err := EncodeStepEnded(e); err == nil {
|
|
t.Errorf("an end outside a pipeline with %s is said", name)
|
|
}
|
|
}
|
|
g14, _ := StepEndedSubject(AskGo, "g14")
|
|
if _, err := DecodeStepEnded(g14, raw); err == nil || !strings.Contains(err.Error(), "said on g14's subject") {
|
|
t.Errorf("an end said on another node's subject: %v", err)
|
|
}
|
|
failed, _ := EndOf(stepAsk(), "ace", OutcomeCouldNotRun)
|
|
if _, err := EncodeStepEnded(failed); err == nil || !strings.Contains(err.Error(), "and not why") {
|
|
t.Errorf("could not run without why: %v", err)
|
|
}
|
|
if _, err := EndOf(goodCheck(), "ace", OutcomePassed); err == nil {
|
|
t.Error("a check has a step's end")
|
|
}
|
|
}
|
|
|
|
func TestDeclinesNeedsAndLevelDigest(t *testing.T) {
|
|
// design 47, the artifact store's holder takes its own new build (test 60): only the store holder's trunk build is
|
|
// kept, and kept-artifact answers the kept one or refuses another digest, a module claiming no store, one not kept.
|
|
t.Run("kept-artifact", func(t *testing.T) {
|
|
holder := catalogue.Manifest{Module: "demo", Claims: []catalogue.Claim{{Name: StoreSeat, Scope: "mesh"}}}
|
|
if KeepOf(true, holder, "fp1") == nil || KeepOf(false, holder, "fp1") != nil || KeepOf(true, catalogue.Manifest{Module: "web"}, "fp1") != nil {
|
|
t.Error("only the store holder's trunk build is kept")
|
|
}
|
|
hex := strings.Repeat("a", 64)
|
|
name, desc := KeptObject(KeptFor{Module: "demo", Fp: "fp1"}, "ace", "sha256:"+hex)
|
|
at := time.Date(2026, 11, 10, 10, 0, 0, 0, time.UTC)
|
|
var kept []KeptBuild
|
|
for _, o := range []struct {
|
|
name, desc string
|
|
size uint64
|
|
}{{"cc", `{"module":"demo","fp":"fp0","node":"ace"}`, 1}, {name, desc, 9}, {"bb", `{"module":"demo","fp":"fp1","node":"g14"}`, 3}, {"dd", "not json", 4}} {
|
|
if k, ok := KeptBuildOf(o.name, o.desc, o.size, at, "ace"); ok {
|
|
kept = append(kept, k)
|
|
}
|
|
}
|
|
SortKept(kept)
|
|
if len(kept) != 2 || kept[0].Digest != hex || kept[0].Bytes != 9 || !kept[0].LastUsed.Equal(at) || kept[1].Digest != "cc" {
|
|
t.Fatalf("kept on ace: %+v", kept)
|
|
}
|
|
claims := func(m string) (bool, error) {
|
|
man := `{"module":"` + m + `"}`
|
|
if m != "other" {
|
|
man = `{"module":"` + m + `","claims":[{"name":"oci-artifact-registry","scope":"mesh"}]}`
|
|
}
|
|
return ClaimsStoreOf(m, []byte(`{"module":{"module":"`+m+`","manifest":`+man+`}}`))
|
|
}
|
|
for _, c := range []struct{ name, module, digest, refused, store string }{
|
|
{"the kept build", "demo", hex, "", "mesh-build-go_kept-builds"},
|
|
{"another digest", "demo", strings.Repeat("b", 64), KeptRefusedDigestDiffers, ""},
|
|
{"a module claiming no store", "other", hex, KeptRefusedClaimsNoStore, ""},
|
|
{"a holder not kept here", "lost", hex, KeptRefusedNotKept, ""},
|
|
} {
|
|
a := KeptArtifact(c.module, "fp1", c.digest, kept, "mesh-build-go_kept-builds", claims)
|
|
if a.Refused != c.refused || a.Store != c.store || a.Error != "" || (c.refused != "" && !strings.Contains(a.Words, c.module)) {
|
|
t.Errorf("%s: %+v", c.name, a)
|
|
}
|
|
}
|
|
if a := KeptArtifact("lost", "fp1", hex, kept, "s", func(string) (bool, error) { return false, errors.New("no answer") }); !strings.Contains(a.Error, "no answer") {
|
|
t.Errorf("an unanswered catalog: %+v", a)
|
|
}
|
|
})
|
|
if got := Lacks([]string{NeedNodeBuildNode}, nil); len(got) != 1 {
|
|
t.Errorf("a nil has lacks all: %v", got)
|
|
}
|
|
if got := Lacks([]string{NeedNodeBuildNode}, func(string) bool { return true }); len(got) != 0 {
|
|
t.Errorf("%v", got)
|
|
}
|
|
step := Step{State: StepRefused, Result: json.RawMessage(`{"declined":true,"needs":["node-build-node"],"lacks":["node-build-node"]}`)}
|
|
if d, ok := DeclinedOf(step); !ok || d.Lacks[0] != NeedNodeBuildNode {
|
|
t.Errorf("%+v %v", d, ok)
|
|
}
|
|
step.State = StepFailed
|
|
if _, ok := DeclinedOf(step); ok {
|
|
t.Error("a failed step is a decline")
|
|
}
|
|
var l LevelAnswer
|
|
raw := `{"level":"unit","digest":{"packages":{"b":"2","a":"1"},"needs":[{"package":"a","needs":["node-build-node"]}]},"unmet":[{"package":"a","needs":["node-build-node"]}]}`
|
|
if err := json.Unmarshal([]byte(raw), &l); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p := l.Digest.Packages; len(p) != 2 || p[0].Package != "a" || l.Digest.Map()["b"] != "2" || l.Digest.NeedsOf("a")[0] != NeedNodeBuildNode {
|
|
t.Errorf("%+v", l.Digest)
|
|
}
|
|
if got := UnmetNeeds([]LevelAnswer{l, l}); len(got) != 1 {
|
|
t.Errorf("%v", got)
|
|
}
|
|
}
|
|
|
|
func TestPauseCapacityAndComposerKey(t *testing.T) {
|
|
raw, err := EncodePause(Pause{Paused: true, By: "operator@shanks", At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p, err := DecodePause(raw); err != nil || !p.Paused || p.V != Version {
|
|
t.Fatalf("%+v %v", p, err)
|
|
}
|
|
if _, err := DecodePause([]byte(`{"v":9}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("%v", err)
|
|
}
|
|
craw, _ := EncodeCapacity(Capacity{Effective: 2, Configured: 2, Why: "fits"})
|
|
if c, err := DecodeCapacity(craw); err != nil || c.Effective != 2 {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
tc := ToolchainUsed{Language: "go", Version: "1.26.0", Recipe: "1"}
|
|
if k := ComposerKey("novox/r", testCommit, "d", "p", tc, nil); k != ComposerKey("novox/r", testCommit, "d", "p", tc, nil) || !strings.HasPrefix(k, "composer-") || len(k) != len("composer-")+16 {
|
|
t.Errorf("composer key %q", k)
|
|
}
|
|
if ComposerKey("novox/r", testCommit, "d", "p", tc, nil) == ComposerKey("novox/r", testCommit, "d2", "p", tc, nil) {
|
|
t.Error("the directory is not in the key")
|
|
}
|
|
// 1921: the asked versions are in the key, in any order of naming; none gives the key without them.
|
|
a, b := strings.Repeat("a", 40), strings.Repeat("b", 40)
|
|
k := ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{"m/x": a, "m/y": b})
|
|
if k != ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{"m/y": b, "m/x": a}) {
|
|
t.Error("the key depends on the order of the versions")
|
|
}
|
|
if k == ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{"m/x": b, "m/y": a}) || k == ComposerKey("novox/r", testCommit, "d", "p", tc, nil) {
|
|
t.Error("other versions, or none, share the key")
|
|
}
|
|
// 1921 (B0): a run's program is kept the same way, under a key of its own kind.
|
|
p := RunProgram{Repository: "novox/mesh-delivery", Commit: testCommit, Dir: "modules/mesh-delivery-checker", Package: "p"}
|
|
if rk := RunKey(p, tc, nil); !strings.HasPrefix(rk, "program-") || len(rk) != len("program-")+16 || rk != RunKey(p, tc, nil) ||
|
|
RunKey(p, tc, PackageVersions{"m/x": a}) == rk {
|
|
t.Errorf("run key = %q", rk)
|
|
}
|
|
if ComposerKey("novox/r", testCommit, "d", "p", tc, nil) != ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{}) {
|
|
t.Error("an empty ask changed the key")
|
|
}
|
|
// Issue 1748 (ADR 0664): a transport fault is told from a wrong source by its words; a step given again is no end; the
|
|
// bus waits 1 minute after the first run and 4 after the second.
|
|
for words, want := range map[string]bool{
|
|
`Get "https://proxy": dial tcp: lookup proxy.golang.org: i/o timeout`: true,
|
|
`Head http://store.internal:5100/v2/x: dial tcp 10.10.0.1:5100: i/o timeout`: true,
|
|
"kex_exchange_identification: read: Connection reset by peer": true,
|
|
"Connection closed by 10.10.0.1 port 222": true,
|
|
"npm error code EAI_AGAIN": true,
|
|
"npm error code ETIMEDOUT": true,
|
|
"npm error code ECONNRESET": true,
|
|
"fatal: unable to access the forge: Could not resolve host: mesh-forge": true,
|
|
"Get \"https://example.com/x.tar.gz\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)": true,
|
|
(&HolderSilent{Seat: "go-package-registry", Holder: "gopkgs", Node: "ace"}).Error(): true,
|
|
`invalid version: Get "https://proxy.golang.org/x/@v/y.info": dial tcp [2a00::1]:443: connect: network is unreachable`: true,
|
|
"dial tcp 10.10.0.1:22: connect: No route to host": true,
|
|
"nats: no responders available for request": false,
|
|
"fatal: unknown revision or path not in the working tree": false,
|
|
"verifying module: checksum mismatch": false,
|
|
"./main.go:3:1: syntax error: unexpected }": false,
|
|
} {
|
|
if PassingCause(words) != want {
|
|
t.Errorf("PassingCause(%q) = %v, want %v", words, !want, want)
|
|
}
|
|
}
|
|
if !IsPassing(fmt.Errorf("fetch: %w", &HolderSilent{Seat: "s"})) || IsPassing(errors.New("i/o timeout")) {
|
|
t.Error("a HolderSilent is a Passing error and a bare error is none")
|
|
}
|
|
if Ended(Step{State: StepRunning}) || Ended(Step{State: StepAgain}) || !Ended(Step{State: StepPassed}) || !Ended(Step{State: StepFailed}) {
|
|
t.Error("Ended: running and again are no end")
|
|
}
|
|
// Review 1: a step ended by the seat's verb drop is the state dropped, an end that decodes (ADR 0664 §4).
|
|
if raw, err := EncodeStep(Step{ID: "mvl0frv18d1990", Tool: "go ask", Node: "ace", State: StepDropped, Reason: "withdrawn"}); err != nil || !Ended(Step{State: StepDropped}) {
|
|
t.Errorf("a dropped step: %v (%s)", err, raw)
|
|
}
|
|
if AgainAfter(1) != time.Minute || AgainAfter(2) != 4*time.Minute {
|
|
t.Errorf("AgainAfter = %v, %v", AgainAfter(1), AgainAfter(2))
|
|
}
|
|
}
|
|
|
|
// The part `build-step` is one layout for the three build seats: each seat's fixtures read strictly in the neutral
|
|
// forms (an unknown field, or a field this package lacks, fails). Where the generator cannot share a part, this is
|
|
// what holds the three seats' types and this form equal.
|
|
func TestTheThreeSeatsFixturesFitTheNeutralForms(t *testing.T) {
|
|
strict := func(t *testing.T, raw json.RawMessage, into any) {
|
|
t.Helper()
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(into); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
seen := 0
|
|
for _, seat := range []string{"node-build-go", "node-build-image", "node-build-node"} {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "mesh-delivery-contracts", "seats", seat, "fixtures", "states.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixtures []struct {
|
|
Name string `json:"name"`
|
|
Of string `json:"of"`
|
|
Given json.RawMessage `json:"given"`
|
|
}
|
|
if err := json.Unmarshal(raw, &fixtures); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range fixtures {
|
|
t.Run(seat+"/"+f.Name, func(t *testing.T) {
|
|
switch {
|
|
case strings.HasPrefix(f.Of, "accept:"):
|
|
seen++
|
|
strict(t, f.Given, &StepAsk{})
|
|
case strings.HasPrefix(f.Of, "state:pauses/paused."):
|
|
seen++
|
|
strict(t, f.Given, &Pause{})
|
|
case strings.HasPrefix(f.Of, "state:pauses/dropped."):
|
|
seen++ // its layout is the generated contracts' Drop, which seatgen checks the fixture against
|
|
case strings.HasPrefix(f.Of, "state:pauses/capacity."):
|
|
seen++
|
|
strict(t, f.Given, &Capacity{})
|
|
case strings.HasPrefix(f.Of, "state:steps/"):
|
|
seen++
|
|
strict(t, f.Given, &Step{})
|
|
var st Step
|
|
if err := json.Unmarshal(f.Given, &st); err == nil && len(st.Result) > 0 {
|
|
// a step's result keeps what the tool answered, whole (the fetch tool's answer too)
|
|
strict(t, st.Result, &StepResult{})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
// 086 A-D31: each seat's step-ended fixtures are the neutral StepEnded, said on their node's subject.
|
|
ended := 0
|
|
for _, seat := range []string{"node-build-go", "node-build-image", "node-build-node"} {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "mesh-delivery-contracts", "seats", seat, "fixtures", "events.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixtures []struct {
|
|
Name string `json:"name"`
|
|
Of string `json:"of"`
|
|
Subject string `json:"subject"`
|
|
Given json.RawMessage `json:"given"`
|
|
}
|
|
if err := json.Unmarshal(raw, &fixtures); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range fixtures {
|
|
if f.Of == "event:step-ended" {
|
|
ended++
|
|
strict(t, f.Given, &StepEnded{})
|
|
if _, err := DecodeStepEnded(f.Subject, f.Given); err != nil {
|
|
t.Errorf("%s/%s: %v", seat, f.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ended < 6 {
|
|
t.Errorf("only %d step-ended fixtures read", ended)
|
|
}
|
|
if seen < 12 {
|
|
t.Errorf("only %d fixtures read", seen)
|
|
}
|
|
}
|
|
|
|
// 1966 (G §465, §466): a build ask's fetch names each package member's build, a built artifact the packages it resolved,
|
|
// and the toolchain the building node's digest, each read whole by the neutral forms.
|
|
func TestFetchBuildsResolvedAndToolchainDigestAreRead(t *testing.T) {
|
|
strict := func(raw string, into any) {
|
|
t.Helper()
|
|
dec := json.NewDecoder(strings.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(into); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
var job BuildJob
|
|
strict(`{"module":"m","fetch":{"repository":"r","commit":"c","builds":[{"package":"example.org/m/go","module":"m","artifact":"go-m","digest":"sha256:aa"}]},"artifacts":[]}`, &job)
|
|
if job.Fetch == nil || len(job.Fetch.Builds) != 1 || job.Fetch.Builds[0] != (FetchBuild{Package: "example.org/m/go", Module: "m", Artifact: "go-m", Digest: "sha256:aa"}) {
|
|
t.Errorf("fetch builds: %+v", job.Fetch)
|
|
}
|
|
var built AskBuilt
|
|
strict(`{"artifact":"go-m","digest":"sha256:bb","resolved":[{"package":"example.org/s/go","version":"0.1.4","digest":"sha256:cc"}]}`, &built)
|
|
if len(built.Resolved) != 1 || built.Resolved[0] != (AskResolved{Package: "example.org/s/go", Version: "0.1.4", Digest: "sha256:cc"}) {
|
|
t.Errorf("resolved: %+v", built)
|
|
}
|
|
var tc ToolchainUsed
|
|
strict(`{"language":"go","recipe":"r","version":"1.26.0","digest":"sha256:dd"}`, &tc)
|
|
if tc.Digest != "sha256:dd" {
|
|
t.Errorf("toolchain digest: %+v", tc)
|
|
}
|
|
}
|
|
|
|
// §481, §600: a queue's ask of an asker is put on accept.<queue>.<asker>, and both forms parse.
|
|
func TestAskSubjectBy(t *testing.T) {
|
|
got, err := AskSubjectBy(AskGo, AskDelivery, "mesh-builds")
|
|
if err != nil || got != "mesh.seat.node-build-go.accept.delivery.mesh-builds" {
|
|
t.Fatalf("%q %v", got, err)
|
|
}
|
|
for _, c := range []struct{ subject, language, kind, asker string }{
|
|
{got, "go", "delivery", "mesh-builds"},
|
|
{"mesh.seat.node-build-go.accept.check.mesh-pipeline-runner", "go", "check", "mesh-pipeline-runner"},
|
|
{"mesh.seat.node-build-node.accept.check", "node", "check", ""},
|
|
} {
|
|
l, k, a, err := ParseAskSubjectBy(c.subject)
|
|
if err != nil || l != c.language || k != c.kind || a != c.asker {
|
|
t.Errorf("%s: %q %q %q %v", c.subject, l, k, a, err)
|
|
}
|
|
if l, k, err := ParseAskSubject(c.subject); err != nil || l != c.language || k != c.kind {
|
|
t.Errorf("%s: ParseAskSubject %q %q %v", c.subject, l, k, err)
|
|
}
|
|
}
|
|
if _, err := AskSubjectBy(AskGo, AskDelivery, "Bad.Name"); err == nil {
|
|
t.Error("a bad asker has a subject")
|
|
}
|
|
if _, _, _, err := ParseAskSubjectBy("mesh.seat.node-build-go.accept.delivery.builder.extra"); err == nil {
|
|
t.Error("a subject of four tokens after accept parses")
|
|
}
|
|
}
|