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" ) 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]bool{} for _, tool := range m0.SeatTools(m0.Puller(nil, buildstep.NewFakeAsks())) { served[tool.Name] = true } for _, verb := range []string{"current", "pause", "resume"} { if !served[Seat+"."+verb] { t.Errorf("%s.%s is not served", Seat, verb) } } }