Files
mesh-sdk/go/delivery/stepasks_test.go
T
jschoubben a20bdf7952 Carry mesh package versions in build asks and fingerprints
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
2026-11-01 02:40:12 +01:00

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)
}
}