Design 47 lines 1888-1906 at b229f5c4 (test 60): the verb sits in every build seat's definition. Its judgement (what may be kept, the kept builds a node holds, the three refusals) is shared code of one domain's modules, so it is the domain SDK's (sdk/buildstep KeptArtifact, ADR 0634 §2): mesh-build-go keeps its store, sweep and tools and drops its own copy of the judgement and its table; mesh-build-node and mesh-build-image keep no build and answer over none, so a module claiming no oci-artifact-registry is refused and the holder is answered not kept. Their manifests serve the verb and invoke seat:mesh-catalog.module, which the judgement asks. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ (Joined from 42d364fe.) Builds: design 47 G §271, §464 (BLD9) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
155 lines
6.5 KiB
Go
155 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
|
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
const askCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
func fakeSteps(calls *[]string) []StepTool {
|
|
answer := func(name string, out any) StepTool {
|
|
return StepTool{Name: name, Run: func(ctx context.Context, args map[string]any) (any, error) {
|
|
*calls = append(*calls, name)
|
|
return out, nil
|
|
}}
|
|
}
|
|
return []StepTool{
|
|
answer("build_node_fetch", map[string]any{"workspace": "ws1", "modules": []string{"modules/gitea"}}),
|
|
answer("build_node_build", map[string]any{"digest": "sha256:bb", "reference": "artifact-store://gitea/code@sha256:bb"}),
|
|
answer("build_node_test", map[string]any{"passed": true}),
|
|
}
|
|
}
|
|
|
|
func nodeAsk(kind string) sdkstep.StepAsk {
|
|
a := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: sdkstep.AskNode, Asker: "mesh-delivery-checker", At: time.Now()}
|
|
fetch := sdkstep.Fetch{Repository: "novox/mesh-catalog", Commit: askCommit}
|
|
if kind == sdkstep.AskDelivery {
|
|
a.Asker = "mesh-delivery-builder"
|
|
a.Build = &sdkstep.BuildJob{Module: "gitea", Path: "modules/gitea", Fetch: &fetch, Artifacts: []sdkstep.AskArtifact{{Artifact: "code"}}}
|
|
} else {
|
|
a.Check = &sdkstep.CheckJob{Fetch: fetch, Levels: []string{sdkstep.LevelUnit}}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// 1887/A3node: a pulled build ask runs fetch then build and its key names the reference; a check ask tests each module
|
|
// directory the fetch named.
|
|
func TestAPulledNodeAskRunsItsJob(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "v26.10.0")
|
|
m.Capacity = func() int { return 1 }
|
|
var calls []string
|
|
src := buildstep.NewFakeAsks()
|
|
build, check := nodeAsk(sdkstep.AskDelivery), nodeAsk(sdkstep.AskCheck)
|
|
_ = src.PublishAsk(check)
|
|
_ = src.PublishAsk(build)
|
|
p := m.Puller(fakeSteps(&calls), src)
|
|
p.PullWait = 10 * time.Millisecond
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
defer stop()
|
|
go p.Pull(ctx)
|
|
for deadline := time.Now().Add(3 * time.Second); ; time.Sleep(5 * time.Millisecond) {
|
|
if acked, _, _ := src.Fates(); len(acked) == 2 {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("the asks were not both acked: calls %v", calls)
|
|
}
|
|
}
|
|
if got := strings.Join(calls, ","); got != "build_node_fetch,build_node_build,build_node_fetch,build_node_test" {
|
|
t.Errorf("calls = %s, want the delivery ask's job first, then the check's", got)
|
|
}
|
|
key, _ := sdkstep.AskStepKey("ace", build.ID)
|
|
states.mu.Lock()
|
|
v := states.steps[key]
|
|
states.mu.Unlock()
|
|
if len(v) == 0 || !strings.Contains(string(v[len(v)-1].Result), "artifact-store://gitea/code@sha256:bb") {
|
|
t.Fatalf("the step %s = %v", key, v)
|
|
}
|
|
}
|
|
|
|
// 1887/A3node, A7: the manifest takes the node's build capacity as a setting, with the builder's default, in a settings
|
|
// file; the seat's verbs are served.
|
|
func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
|
m := readManifest(t)
|
|
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
|
var doc struct {
|
|
Config map[string]struct {
|
|
Default json.RawMessage `json:"default"`
|
|
Pattern, Description string
|
|
} `json:"config"`
|
|
}
|
|
_ = json.Unmarshal(raw, &doc)
|
|
// Issue 1806's follow-up: the cores a step needs is a setting of its own, in the settings file, with the SDK's default.
|
|
if cores := string(doc.Config[buildstep.CoresPerStepSetting].Default); cores != fmt.Sprint(buildstep.DefaultCoresPerStep[sdkstep.AskNode]) {
|
|
t.Errorf("the cores-per-step default = %s, want %g", cores, buildstep.DefaultCoresPerStep[sdkstep.AskNode])
|
|
}
|
|
if got := buildstep.NodeCoresPerStep(moduleDir+"/files/settings.json", 0, nil)(); got != buildstep.DefaultCoresPerStep[sdkstep.AskNode] {
|
|
t.Errorf("the settings template holds cores-per-step %g", got)
|
|
}
|
|
// And the build hours (the operator, 2026-10-26): a setting of their own, empty by default (any hour), in the template.
|
|
if hours := string(doc.Config[buildstep.BuildHoursSetting].Default); hours != `""` {
|
|
t.Errorf("the build-hours default = %s, want empty", hours)
|
|
}
|
|
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
|
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
|
}
|
|
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
|
window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$")
|
|
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
|
if window.MatchString(value) != ok {
|
|
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
|
}
|
|
}
|
|
if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") {
|
|
t.Errorf("the build-hours description names a node pair")
|
|
}
|
|
var capacity float64
|
|
_ = json.Unmarshal(doc.Config[sdkstep.CapacitySetting].Default, &capacity)
|
|
if capacity != 0 {
|
|
t.Errorf("the capacity's default = %g, want 0", capacity)
|
|
}
|
|
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
|
t.Error("no settings file")
|
|
}
|
|
m0 := &Module{Node: "ace", Capacity: func() int { return 0 }}
|
|
served := map[string]stdio.Tool{}
|
|
for _, tool := range m0.SeatTools(m0.Puller(nil, buildstep.NewFakeAsks())) {
|
|
served[tool.Name] = tool
|
|
}
|
|
for _, verb := range []string{"current", "pause", "resume", "kept-artifact"} {
|
|
if _, ok := served[Seat+"."+verb]; !ok {
|
|
t.Errorf("%s.%s is not served", Seat, verb)
|
|
}
|
|
}
|
|
// Design 47 lines 1888-1906 (test 60): kept-artifact is the domain SDK's judgement over no kept build here: the
|
|
// store's holder is not kept, any other module claims no oci-artifact-registry.
|
|
m0.Call = func(key string, body any) (json.RawMessage, error) {
|
|
mod, _ := body.(map[string]any)["module"].(string)
|
|
man := `{"module":"` + mod + `"}`
|
|
if mod == "holder" {
|
|
man = `{"module":"holder","claims":[{"name":"oci-artifact-registry","scope":"mesh"}]}`
|
|
}
|
|
return json.RawMessage(`{"module":{"module":"` + mod + `","manifest":` + man + `}}`), nil
|
|
}
|
|
for mod, refused := range map[string]string{"holder": "not kept", "web": "claims no oci-artifact-registry"} {
|
|
if v, ok := served[Seat+".kept-artifact"]; ok {
|
|
out, _ := v.Run(map[string]any{"module": mod, "fp": "fp1", "digest": strings.Repeat("a", 64)})
|
|
if a, _ := out.(sdkstep.KeptArtifactAnswer); a.Refused != refused || !strings.Contains(a.Words, mod) {
|
|
t.Errorf("kept-artifact of %s answered %+v, want refused %q", mod, out, refused)
|
|
}
|
|
}
|
|
}
|
|
}
|