Ported from the dev branch (192f6ab2, 4f013f44, dc0df1b5) into the package sdk/buildstep where they were built; the package is not named by a design line (see below). Builds: design 47 G §271 (a module claiming oci-artifact-registry but not its holder in the seat list is refused), §24 (a step's end is published on its node's subject), §416 (the step ask carries the kept results), §370 (the version). Not decided: the package of SD2 and SD3. G-9 §420 names sdk/steprun only for the step-run code (SD1); no line places the kept-artifact judgement or the step ask. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
550 lines
26 KiB
Go
550 lines
26 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"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): the names the lint step reads and a 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},
|
|
Names: StepNames{Modules: []string{"nats"}, Repositories: []string{"novox/mesh-catalog"}, Branches: []string{"main"}},
|
|
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) {
|
|
for _, a := range []StepAsk{goodBuild(), goodCheck(), runAsk(), 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 no head", func(a *StepAsk) { a.Run.Heads = nil }, "no head"},
|
|
{"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 over its bound", func(a *StepAsk) { a.Run.Input.Size = RunInputBound + 1 }, "outside 1 to"},
|
|
{"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"},
|
|
{"no names", func(a *StepAsk) { a.Step.Names = StepNames{Modules: []string{"nats"}} }, "no repositories, branches of the registrations"},
|
|
{"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")
|
|
}
|
|
if _, err := RunInputOf("seat:mesh-delivery-checker.input", make([]byte, RunInputBound+1)); err == nil || !strings.Contains(err.Error(), "262145 bytes") {
|
|
t.Errorf("an input over its bound: %v", 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")
|
|
}
|
|
if k, err := AskStepKey("ace", testAskID); err != nil || k != "ace."+testAskID {
|
|
t.Errorf("%q %v", k, err)
|
|
}
|
|
if w, _ := AskStepWatch(testAskID); w != "*."+testAskID {
|
|
t.Errorf("watch %q", w)
|
|
}
|
|
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 {
|
|
t.Errorf("round trip: %+v %v", back, err)
|
|
}
|
|
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)
|
|
kept := KeptBuildsOf([]buscalls.ObjectInfo{{Name: name, Size: 9, Description: desc, Modified: at},
|
|
{Name: "bb", Description: `{"module":"demo","fp":"fp1","node":"g14"}`}}, "ace")
|
|
if len(kept) != 1 || kept[0].Digest != hex || kept[0].Bytes != 9 {
|
|
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("..", "..", "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("..", "..", "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)
|
|
}
|
|
}
|