Builder's unit tests run an ask with a local runner, not mesh-sdk's

The build runner leaves mesh-sdk for mesh-build-go (ADR 0593 §1), another
repository the builder does not import; the tests answer asks as the builder
needs them answered.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 18:31:07 +01:00
parent 604f1528d6
commit f6fa75e189
3 changed files with 101 additions and 8 deletions
@@ -0,0 +1,94 @@
package build
import (
"context"
"encoding/json"
"fmt"
"maps"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The build runner a build node runs an ask with is mesh-build-go's (novox/hq ADR 0593 §1), another repository's,
// which the builder does not import. These tests run an ask as the builder needs it run: fetch once at the job's
// fetch arguments with its package versions, build or copy each artifact, answer each one's digest and reference. They
// test the builder, not the runner.
// stepFunc is one step of a build module, as its tool answers it.
type stepFunc func(ctx context.Context, args map[string]any) (any, error)
// askSteps are a build module's steps an ask is run with.
type askSteps struct{ Fetch, Build, Copy stepFunc }
// runAsk runs a build ask with steps and answers its delivery.BuildAnswer, or the error that fails the step.
func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, error) {
if ask.Build == nil {
return nil, fmt.Errorf("the ask %s holds no build job", ask.ID)
}
job := *ask.Build
fetch := job.Fetch
if len(ask.Packages) > 0 && fetch != nil {
fetch = maps.Clone(fetch)
fetch["packages"] = maps.Clone(ask.Packages)
}
answer := delivery.BuildAnswer{}
workspace := ""
for _, a := range job.Artifacts {
var out any
var err error
if a.From != "" {
if steps.Copy == nil {
return nil, fmt.Errorf("this build module copies no artifact (%s)", a.Artifact)
}
out, err = steps.Copy(ctx, map[string]any{"module": job.Module, "artifact": a.Artifact, "from": a.From})
} else {
if workspace == "" {
raw, err := asRaw(steps.Fetch(ctx, fetch))
if err != nil {
return nil, err
}
var f struct {
Workspace string `json:"workspace"`
Merge string `json:"merge"`
Conflicts []string `json:"conflicts"`
}
if err := json.Unmarshal(raw, &f); err != nil || f.Workspace == "" {
return nil, fmt.Errorf("the fetch answered no workspace: %s", raw)
}
if _, candidate := fetch["base"]; candidate && (len(f.Conflicts) > 0 || f.Merge == "") {
return nil, fmt.Errorf("the head does not merge into the trunk: %s", strings.Join(f.Conflicts, ", "))
}
workspace = f.Workspace
}
args := map[string]any{"workspace": workspace, "module": job.Path, "artifact": a.Artifact}
if job.Publish {
args["publish"] = true
}
out, err = steps.Build(ctx, args)
}
raw, err := asRaw(out, err)
if err != nil {
return nil, fmt.Errorf("%s: %w", a.Artifact, err)
}
var built struct {
Digest string `json:"digest"`
Reference string `json:"reference"`
}
if err := json.Unmarshal(raw, &built); err != nil || built.Digest == "" {
return nil, fmt.Errorf("the build of %s answered no digest: %s", a.Artifact, raw)
}
answer.Artifacts = append(answer.Artifacts, delivery.AskBuilt{Artifact: a.Artifact, Digest: built.Digest, Reference: built.Reference})
}
return answer, nil
}
func asRaw(v any, err error) (json.RawMessage, error) {
if err != nil {
return nil, err
}
if r, ok := v.(json.RawMessage); ok {
return r, nil
}
return json.Marshal(v)
}
@@ -29,7 +29,7 @@ func captured(t *testing.T, name string) json.RawMessage {
return step.Result
}
// The captured image steps, run as a pulled ask runs them (buildstep.RunAsk), answer what the builder reads: the trunk's
// The captured image steps, run as a pulled ask runs them (runAsk), answer what the builder reads: the trunk's
// build its digest and reference, a candidate's its digest and no reference (issue 1887 A4a).
func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // review r2, mesh-catalog 2; ADR 0487 clause 1
for _, tc := range []struct {
@@ -37,7 +37,7 @@ func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // revi
publish bool
ref bool
}{{"build-trunk.json", true, true}, {"build-candidate.json", false, false}} {
steps := buildstep.AskSteps{
steps := askSteps{
Fetch: func(context.Context, map[string]any) (any, error) {
return json.RawMessage(captured(t, "fetch.json")), nil
},
@@ -47,7 +47,7 @@ func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // revi
At: time.Now(), Build: &delivery.BuildJob{Module: "m", Path: "modules/m", Publish: tc.publish,
Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": strings.Repeat("a", 40)},
Artifacts: []delivery.AskArtifact{{Artifact: "server"}}}}
out, err := buildstep.RunAsk(context.Background(), steps, ask)
out, err := runAsk(context.Background(), steps, ask)
if err != nil {
t.Fatalf("%s: %v", tc.build, err)
}
@@ -15,7 +15,6 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -140,7 +139,7 @@ type rig struct {
// toolPrefix is the start of each build module's tool names, as the fake build modules answer them.
var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_", ImageBuildModule: "build_image_"}
// answer runs an ask as the build module that pulls it does (buildstep.RunAsk), with the fake build modules' tools on the
// answer runs an ask as the build module that pulls it does (runAsk), with the fake build modules' tools on the
// rig's pulling node, and answers the step that node writes.
func (r *rig) answer(a delivery.StepAsk) delivery.Step {
node := r.puller
@@ -149,7 +148,7 @@ func (r *rig) answer(a delivery.StepAsk) delivery.Step {
}
module, _ := delivery.BuildModuleOf(a.Language)
runner := buildrun.Runner{Caller: r.bm, Steps: r.bm.steps, Bound: time.Second}
tool := func(name string) buildstep.StepFunc {
tool := func(name string) stepFunc {
return func(ctx context.Context, args map[string]any) (any, error) {
raw, err := runner.Run(ctx, module, node, toolPrefix[module]+name, args)
if err != nil {
@@ -158,8 +157,8 @@ func (r *rig) answer(a delivery.StepAsk) delivery.Step {
return raw, nil
}
}
steps := buildstep.AskSteps{Fetch: tool("fetch"), Build: tool("build"), Copy: tool("copy")}
out, err := buildstep.RunAsk(context.Background(), steps, a)
steps := askSteps{Fetch: tool("fetch"), Build: tool("build"), Copy: tool("copy")}
out, err := runAsk(context.Background(), steps, a)
step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: delivery.StepPassed}
if err != nil {
step.State, step.Reason = delivery.StepFailed, err.Error()