ADR 0573 §2 has every module build against the latest published version of each mesh package it uses, chosen by the planner and written by the build, never pinned by hand (issue 1585, S4). The ask, the fetch request and the merge preview now carry the packages and their versions, and the new-form fingerprint takes each version, so a package's trunk merge changes every dependent's fingerprint. A module using no package keeps its fingerprint. Folded onto feat/judge-once by builder-1585a: Module keeps both Packages and the verdict fields. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
217 lines
8.9 KiB
Go
217 lines
8.9 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const askCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
func buildAsk() StepAsk {
|
|
return StepAsk{ID: "mh3k2x9qa1b2c3", Kind: AskDelivery, Language: AskGo, Asker: "mesh-delivery-builder",
|
|
At: time.Date(2026, 10, 23, 13, 0, 0, 0, time.UTC),
|
|
Build: &BuildJob{Module: "mesh-delivery-view", Path: "modules/mesh-delivery-view", FP: "ab12",
|
|
Fetch: map[string]any{"repository": "novox/mesh-delivery", "commit": askCommit, "dir": "modules/mesh-delivery-view", "paths": []any{"cmd"}},
|
|
Artifacts: []AskArtifact{{Artifact: "bundle"}, {Artifact: "base", From: "registry/x@sha256:00"}},
|
|
Publish: true, Toolchain: &ToolchainUsed{Version: "go1.26.0", Recipe: "3"}},
|
|
Packages: PackageVersions{"git.novox.be/novox/mesh-sdk/go": askCommit}}
|
|
}
|
|
|
|
func checkAsk() StepAsk {
|
|
return StepAsk{ID: "mh3k2x9qa1b2c4", Kind: AskCheck, Language: AskNode, Asker: "mesh-delivery-checker",
|
|
At: time.Date(2026, 10, 23, 13, 0, 0, 0, time.UTC),
|
|
Check: &CheckJob{Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": askCommit, "ref": "refs/pull/3/head", "base": askCommit},
|
|
Levels: []string{LevelUnit, LevelIntegration}, Kept: true, Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Run: "",
|
|
NodeNames: []string{"anchor", "laptop"}},
|
|
Packages: PackageVersions{"git.novox.be/novox/mesh-sdk/go": askCommit, "git.novox.be/novox/mesh-controller/modules/route-proxy/contract": askCommit}}
|
|
}
|
|
|
|
// 1887/A1a: an ask encodes and decodes with every field; 1585/S4: the package versions too.
|
|
func TestAskRoundTrips(t *testing.T) {
|
|
for _, a := range []StepAsk{buildAsk(), checkAsk()} {
|
|
raw, err := EncodeStepAsk(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DecodeStepAsk(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.V = StepAskVersion
|
|
if !reflect.DeepEqual(got, a) {
|
|
t.Fatalf("round trip:\n got %+v\nwant %+v", got, a)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1887/A1a: an ask with an unknown kind or language is refused.
|
|
func TestAskRefusesUnknownKindAndLanguage(t *testing.T) {
|
|
cases := map[string]func(*StepAsk){
|
|
"kind": func(a *StepAsk) { a.Kind = "build" },
|
|
"language": func(a *StepAsk) { a.Language = "typescript" },
|
|
"id": func(a *StepAsk) { a.ID = "postgres" },
|
|
"neither": func(a *StepAsk) { a.Build = nil },
|
|
"both": func(a *StepAsk) { a.Check = &CheckJob{Levels: []string{LevelUnit}, Fetch: a.Build.Fetch} },
|
|
"recheck": func(a *StepAsk) { a.Kind = AskRecheck },
|
|
"commit": func(a *StepAsk) { a.Build.Fetch = map[string]any{"repository": "novox/x", "commit": "main"} },
|
|
"path": func(a *StepAsk) { a.Build.Path = "" },
|
|
"needs": func(a *StepAsk) { a.Needs = []string{NeedNodeBuildNode} }, // issue 890: a build needs no node
|
|
// Issue 1585: a package's version is a full commit, of a module path.
|
|
"version": func(a *StepAsk) { a.Packages["git.novox.be/novox/mesh-sdk/go"] = "v0.1.7" },
|
|
"package": func(a *StepAsk) { a.Packages["git.novox.be/novox/x y"] = askCommit },
|
|
"compose": func(a *StepAsk) {
|
|
a.Kind, a.Build = AskCheck, nil
|
|
a.Compose = &ComposeJob{Fetch: map[string]any{"repository": "novox/mesh-sdk", "commit": askCommit}, Dir: "go", Package: "x", Request: json.RawMessage(`{}`)}
|
|
},
|
|
}
|
|
for name, change := range cases {
|
|
a := buildAsk()
|
|
change(&a)
|
|
if _, err := EncodeStepAsk(a); err == nil {
|
|
t.Errorf("%s: encoded", name)
|
|
}
|
|
}
|
|
// ADR 0567 §12: a check's node names are each a node name, once.
|
|
for _, names := range [][]string{{"anchor", "anchor"}, {"a.b"}, {""}, {"a,b"}} {
|
|
c := checkAsk()
|
|
c.Check.NodeNames = names
|
|
if _, err := EncodeStepAsk(c); err == nil {
|
|
t.Errorf("the node names %q: encoded", names)
|
|
}
|
|
}
|
|
// A copy alone fetches nothing and needs no path.
|
|
a := buildAsk()
|
|
a.Build.Fetch, a.Build.Path, a.Build.Artifacts = nil, "", []AskArtifact{{Artifact: "base", From: "registry/x@sha256:00"}}
|
|
if _, err := EncodeStepAsk(a); err != nil {
|
|
t.Errorf("a copy alone: %v", err)
|
|
}
|
|
c := checkAsk()
|
|
c.Kind = AskDelivery
|
|
if _, err := EncodeStepAsk(c); err == nil {
|
|
t.Error("a check on the delivery stream encoded")
|
|
}
|
|
c = checkAsk()
|
|
c.Language = AskImage
|
|
if _, err := EncodeStepAsk(c); err == nil {
|
|
t.Error("a check of an image encoded")
|
|
}
|
|
// Issue 890: a check names a known need and round-trips it; an unknown need is refused.
|
|
c = checkAsk()
|
|
c.Check.Packages = []string{"a b"}
|
|
if _, err := EncodeStepAsk(c); err == nil {
|
|
t.Error("a package that is no import path encoded")
|
|
}
|
|
c.Check.Packages = []string{"x/pkg/a"}
|
|
c.Needs = []string{"gpu"}
|
|
if _, err := EncodeStepAsk(c); err == nil {
|
|
t.Error("an unknown need encoded")
|
|
}
|
|
c.Needs = []string{NeedNodeBuildNode}
|
|
if raw, err := EncodeStepAsk(c); err != nil {
|
|
t.Errorf("a check needing %s: %v", NeedNodeBuildNode, err)
|
|
} else if back, err := DecodeStepAsk(raw); err != nil || len(back.Needs) != 1 || back.Needs[0] != NeedNodeBuildNode {
|
|
t.Errorf("the need did not round-trip: %+v %v", back.Needs, err)
|
|
}
|
|
raw, _ := EncodeStepAsk(buildAsk())
|
|
for _, bad := range []string{
|
|
strings.Replace(string(raw), `"kind":"delivery"`, `"kind":"queue"`, 1),
|
|
strings.Replace(string(raw), `"language":"go"`, `"language":"rust"`, 1),
|
|
} {
|
|
if _, err := DecodeStepAsk([]byte(bad)); err == nil {
|
|
t.Errorf("decoded %s", bad)
|
|
}
|
|
}
|
|
if _, err := DecodeStepAsk([]byte(strings.Replace(string(raw), `"v":1`, `"v":2`, 1))); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("version 2: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1887/A1a: the subjects, stream and workers are the seat's, as the broker derives them.
|
|
func TestAskSubjects(t *testing.T) {
|
|
s, err := AskSubject(AskGo, AskCheck)
|
|
if err != nil || s != "mesh.seat.node-build-go.accept.check" {
|
|
t.Fatalf("AskSubject = %q, %v", s, err)
|
|
}
|
|
language, kind, err := ParseAskSubject("mesh.seat.node-build-image.accept.delivery")
|
|
if err != nil || language != AskImage || kind != AskDelivery {
|
|
t.Fatalf("ParseAskSubject = %q %q %v", language, kind, err)
|
|
}
|
|
for _, bad := range []string{"mesh.seat.node-build-agent.accept.build", "mesh.seat.node-build-go.accept.queue", "mesh.seat.node-build-go.event.x"} {
|
|
if _, _, err := ParseAskSubject(bad); err == nil {
|
|
t.Errorf("parsed %s", bad)
|
|
}
|
|
}
|
|
if st, _ := AskStream(AskNode); st != "SEAT_NODE_BUILD_NODE" {
|
|
t.Errorf("AskStream = %q", st)
|
|
}
|
|
if w, _ := AskWorker(AskGo, AskRecheck); w != "SEAT_NODE_BUILD_GO_RECHECK_worker" {
|
|
t.Errorf("AskWorker = %q", w)
|
|
}
|
|
if seat, _ := BuildSeat(AskImage); seat != "node-build-image" {
|
|
t.Errorf("BuildSeat = %q", seat)
|
|
}
|
|
if m, _ := BuildModuleOf(AskNode); m != "mesh-build-node" {
|
|
t.Errorf("BuildModuleOf = %q", m)
|
|
}
|
|
if _, err := BuildSeat("agent"); err == nil {
|
|
t.Error("BuildSeat(agent)")
|
|
}
|
|
}
|
|
|
|
// 1887/A4b: a building key names the asks put for it; a done key or a name that is no ask id is refused.
|
|
func TestABuildingKeyNamesItsAsks(t *testing.T) {
|
|
b := BuilderBuild{State: BuildBuilding, By: "novox.builder", Asks: map[string]string{AskGo: "mh3k2x9qa1b2c3"}}
|
|
raw, err := EncodeBuilderBuild(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DecodeBuilderBuild(raw)
|
|
if err != nil || len(got.Asks) != 1 || got.Asks[AskGo] != "mh3k2x9qa1b2c3" {
|
|
t.Fatalf("decoded %+v, %v", got, err)
|
|
}
|
|
for _, bad := range []BuilderBuild{
|
|
{State: BuildBuilding, Asks: map[string]string{AskGo: "postgres"}},
|
|
{State: BuildBuilding, Asks: map[string]string{"typescript": "mh3k2x9qa1b2c3"}},
|
|
{State: BuildFailed, Error: "x", Asks: map[string]string{AskGo: "mh3k2x9qa1b2c3"}},
|
|
} {
|
|
if _, err := EncodeBuilderBuild(bad); err == nil {
|
|
t.Errorf("encoded %+v", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1930/AC1: a level answer stored before issue 1930, its digest's packages a map keyed by package path, and the digest
|
|
// tool's own answer, read as the list sorted by package; the list form reads back the same and keys no field by a path.
|
|
func TestALevelDigestReadsTheMapAndTheList(t *testing.T) {
|
|
var old LevelAnswer
|
|
if err := json.Unmarshal([]byte(`{"level":"unit","digest":{"level":"unit","packages":{"x/pkg/a":"d-a","x/internal/token":"d-t"},"beside":["novox/y abc"],"judged_by":"installed"}}`), &old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []PackageDigest{{Package: "x/internal/token", Digest: "d-t"}, {Package: "x/pkg/a", Digest: "d-a"}}
|
|
if old.Digest == nil || !reflect.DeepEqual(old.Digest.Packages, want) || old.Digest.JudgedBy != "installed" || len(old.Digest.Beside) != 1 {
|
|
t.Fatalf("the old form read %+v", old.Digest)
|
|
}
|
|
raw, err := json.Marshal(old)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), `"x/internal/token":`) {
|
|
t.Errorf("the list form keys a field by a package path: %s", raw)
|
|
}
|
|
var back LevelAnswer
|
|
if err := json.Unmarshal(raw, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(back.Digest, old.Digest) || back.Digest.Map()["x/internal/token"] != "d-t" {
|
|
t.Errorf("the list form read back %+v, want %+v", back.Digest, old.Digest)
|
|
}
|
|
var none LevelAnswer
|
|
if err := json.Unmarshal([]byte(`{"level":"unit"}`), &none); err != nil || none.Digest != nil || none.Digest.Map() != nil {
|
|
t.Errorf("a level without a digest read %+v, %v", none.Digest, err)
|
|
}
|
|
}
|