From fa73674e22023a3803dd3364a7f2e7e1db796b99 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 22:20:25 +0100 Subject: [PATCH] A run ask builds a checking program once and runs it over its heads and input; compose keeps seats (issue 1921, C1-C3) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-build-go/buildstep/pull_test.go | 5 +- .../mesh-build-go/cmd/mesh-build-go/asks.go | 3 + .../cmd/mesh-build-go/compose.go | 27 ++-- .../mesh-build-go/cmd/mesh-build-go/main.go | 1 + .../mesh-build-go/cmd/mesh-build-go/module.go | 6 +- .../mesh-build-go/cmd/mesh-build-go/run.go | 136 ++++++++++++++++++ .../cmd/mesh-build-go/run_integration_test.go | 105 ++++++++++++++ .../cmd/mesh-build-go/run_test.go | 106 ++++++++++++++ modules/mesh-build-go/module.json | 1 + 9 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/run.go create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/run_test.go diff --git a/modules/mesh-build-go/buildstep/pull_test.go b/modules/mesh-build-go/buildstep/pull_test.go index d196d8eec..9a33b5a4c 100644 --- a/modules/mesh-build-go/buildstep/pull_test.go +++ b/modules/mesh-build-go/buildstep/pull_test.go @@ -541,7 +541,10 @@ func TestPullAnswersAnAskItCannotRead(t *testing.T) { src.Publish(delivery.AskCheck, odd) src.Publish(delivery.AskCheck, []byte(`{"v":1}`)) p := &Puller{Source: src, Language: delivery.AskGo, Node: "ace", Put: st.put, Capacity: func() int { return 1 }, - PullWait: 10 * time.Millisecond, Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { t.Error("an unread ask ran"); return nil, nil }} + PullWait: 10 * time.Millisecond, Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { + t.Error("an unread ask ran") + return nil, nil + }} ctx, cancel := context.WithCancel(context.Background()) defer cancel() go p.Pull(ctx) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/asks.go b/modules/mesh-build-go/cmd/mesh-build-go/asks.go index 5579cc6c8..3ddcb7c43 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/asks.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/asks.go @@ -121,6 +121,9 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep if a.Compose != nil { return m.compose(ctx, ask.Fetch, a) } + if a.Run != nil { + return m.run(ctx, ask.Fetch, a) + } return buildstep.RunAsk(ctx, ask, a) }} } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/compose.go b/modules/mesh-build-go/cmd/mesh-build-go/compose.go index a8f52804f..08688be00 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/compose.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/compose.go @@ -98,7 +98,14 @@ func (m *Module) manifestOf(digest string) ([]byte, error) { // composerOf is the composer kept under key, or, when none is, the one built now from the fetched commit (built true). func (m *Module) composerOf(ctx context.Context, fetch buildstep.StepFunc, job delivery.ComposeJob, key string) (string, bool, error) { - exe := m.composerPath(key) + return m.programOf(ctx, fetch, "composers", "composer", key, job.Fetch, job.Dir, job.Package) +} + +// programOf is the program kept under key in the cache directory kind, or, when none is, the one built now: the commit +// fetch names is fetched (the fetch step: the forge key and the network, none of the change's code), and package in the +// Go module of dir is built in a run step shut off with nothing fetched. what names it in a failure. +func (m *Module) programOf(ctx context.Context, fetch buildstep.StepFunc, kind, what, key string, fetchArgs map[string]any, dir, pkg string) (string, bool, error) { + exe := m.programPath(kind, key) mu := lockOf(exe) mu.Lock() defer mu.Unlock() @@ -109,7 +116,7 @@ func (m *Module) composerOf(ctx context.Context, fetch buildstep.StepFunc, job d if fetch == nil { return "", false, buildstep.Refuse("this build module fetches nothing") } - out, err := fetch(ctx, job.Fetch) + out, err := fetch(ctx, fetchArgs) if err != nil { return "", false, err } @@ -126,11 +133,11 @@ func (m *Module) composerOf(ctx context.Context, fetch buildstep.StepFunc, job d defer m.releaseSaid(f.Workspace) done := m.spaces().Hold(f.Workspace) defer done() - dir := filepath.Join(m.Work, f.Workspace, "tree", job.Dir) - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil { - return "", false, fmt.Errorf("%s at %s holds no go.mod in %s, so no composer can be built there", job.Fetch["repository"], job.Fetch["commit"], job.Dir) + tree := filepath.Join(m.Work, f.Workspace, "tree", dir) + if _, err := os.Stat(filepath.Join(tree, "go.mod")); err != nil { + return "", false, fmt.Errorf("%s at %s holds no go.mod in %s, so no %s can be built there", fetchArgs["repository"], fetchArgs["commit"], dir, what) } - into := filepath.Join(m.Work, f.Workspace, "composer", "mesh-compose") + into := filepath.Join(m.Work, f.Workspace, what, "built") if err := os.MkdirAll(filepath.Dir(into), 0o700); err != nil { return "", false, err } @@ -138,15 +145,15 @@ func (m *Module) composerOf(ctx context.Context, fetch buildstep.StepFunc, job d if err != nil { return "", false, err } - env := append(m.goEnvFor(dir), "GOPROXY=off", "GOTOOLCHAIN=local", "CGO_ENABLED=0") - r := run(ctx, env, "go", "-C", dir, "build", "-trimpath", "-o", into, job.Package) + env := append(m.goEnvFor(tree), "GOPROXY=off", "GOTOOLCHAIN=local", "CGO_ENABLED=0") + r := run(ctx, env, "go", "-C", tree, "build", "-trimpath", "-o", into, pkg) if r.Status != 0 || r.Err != "" { // The words Go says when the module holds no such package (a mesh-sdk from before the composer) are kept whole, // so the asker tells that apart from a fault. - return "", false, fmt.Errorf("the composer %s does not build at %s: %s", job.Package, job.Fetch["commit"], compileError(r.Err+"\n"+r.Stderr+"\n"+r.Stdout)) + return "", false, fmt.Errorf("the %s %s does not build at %s: %s", what, pkg, fetchArgs["commit"], compileError(r.Err+"\n"+r.Stderr+"\n"+r.Stdout)) } if err := copyExecutable(into, exe); err != nil { - return "", false, fmt.Errorf("the composer could not be kept: %w", err) + return "", false, fmt.Errorf("the %s could not be kept: %w", what, err) } return exe, true, nil } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/main.go b/modules/mesh-build-go/cmd/mesh-build-go/main.go index 4e96528b5..03a21b9af 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -58,6 +58,7 @@ func main() { m.Settings = m.Home + "/settings.json" m.ReadKept = readOf(stdio.State(KeptState)).Get m.ReadManifest = readOf(stdio.State(ManifestsState)).Get + m.ReadInput = readOf(stdio.State(InputsState)).Get m.Probe = ProbeGo(m.Home, m.Cache) m.Bubblewrap = ProbeBubblewrap // Outside a launch by the runtime — a build, a check — there is no node and no state to write. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/module.go b/modules/mesh-build-go/cmd/mesh-build-go/module.go index 38e2f843c..6cf40a48e 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -93,7 +93,11 @@ type Module struct { // ReadManifest reads a key of the delivery indexer's manifests state (ManifestsState), which a compose ask's request // names its manifests by (compose.go); GoModCache, when set, is the Go module cache in place of Cache's (a test). ReadManifest func(key string) (json.RawMessage, error) - GoModCache string + // ReadInput reads a key of the checker's inputs state (InputsState), which a run ask's input names by its digest. + ReadInput func(key string) (json.RawMessage, error) + // PlaceHead, when set, lays a run's head at a directory instead of fetching it (a test). + PlaceHead func(ctx context.Context, h delivery.RunHead, dir string) error + GoModCache string spacesOnce sync.Once workspaces *buildstep.Workspaces diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run.go b/modules/mesh-build-go/cmd/mesh-build-go/run.go new file mode 100644 index 000000000..ce14ea74a --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/run.go @@ -0,0 +1,136 @@ +package main + +// **A run ask runs a program built from a change, over heads it reads** (novox/hq ADR 0644, ADR 0656; issue 1921): the +// checking program of a group's check. The module builds the program at the commit the ask names, once per +// delivery.RunKey, as a compose ask builds a composer (programOf); fetches every head the ask names and lays each read-only +// under the directory MESH_RUN_HEADS names, at `/`; reads the input the ask names by its digest from the +// checker's inputs state and writes it to the file MESH_RUN_INPUT names; and runs the program shut off with the ask's +// arguments: no network, none of the account. The answer is delivery.RunAnswer: the program's exit, the last line of its +// standard output and the first line of its standard error. A program that fails is an answer, not a fault of the step. + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// InputsState is the checker's state that keeps a run's input by its digest (the module reads it, module.json reads). +const InputsState = delivery.CheckerModule + ".inputs" + +// run runs a run ask: the program of its commit, built once, over its heads and its input. +func (m *Module) run(ctx context.Context, fetch buildstep.StepFunc, a delivery.StepAsk) (any, error) { + job := *a.Run + if job.Input.State != InputsState { + return nil, buildstep.Refuse("the run ask %s keeps its input in %s; this module reads %s", a.ID, job.Input.State, InputsState) + } + body, err := m.inputOf(job.Input) + if err != nil { + return nil, err + } + installed, err := m.Probe(ctx) + if err != nil { + return nil, err + } + key := delivery.RunKey(job.Program, delivery.ToolchainUsed{Version: installed, Recipe: m.Recipe.Version()}, a.Packages) + fetchArgs := sdkstep.FetchRequest{Repository: job.Program.Repository, Commit: job.Program.Commit, Packages: a.Packages}.Args() + exe, built, err := m.programOf(ctx, fetch, "programs", "checking program", key, fetchArgs, job.Program.Dir, job.Program.Package) + if err != nil { + return nil, err + } + ans, err := m.runProgram(ctx, exe, job, body) + if err != nil { + return nil, err + } + ans.Built, ans.Key = built, key + return ans, nil +} + +// inputOf is the input a run names, read from the checker's inputs state and checked against its digest and size. +func (m *Module) inputOf(in delivery.RunInput) ([]byte, error) { + if m.ReadInput == nil { + return nil, errors.New("this module reads no inputs state") + } + raw, err := m.ReadInput(in.Digest) + if err != nil { + return nil, err + } + if raw == nil { + return nil, fmt.Errorf("the checker keeps no input of digest %s", in.Digest) + } + if err := in.Verify(raw); err != nil { + return nil, buildstep.Refuse("%v", err) + } + return raw, nil +} + +// placeHead lays one head's commit as a plain directory at dir, with no symlink in it; a test sets its own. +func (m *Module) placeHead(ctx context.Context, h delivery.RunHead, dir string) error { + if m.PlaceHead != nil { + return m.PlaceHead(ctx, h, dir) + } + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + if err := buildstep.FetchCommit(ctx, m.Run, dir, m.remote(h.Repository), h.Commit, "", m.fetchOptions(ctx, h.Repository, h.Commit, "")...); err != nil { + return err + } + if err := m.dissociate(ctx, dir); err != nil { + return err + } + if link := firstSymlink(dir); link != "" { + return fmt.Errorf("%s holds a symlink, %s", h.Repository, link) + } + return nil +} + +// runProgram lays the heads and the input in a workspace of its own and runs exe shut off over them. +func (m *Module) runProgram(ctx context.Context, exe string, job delivery.RunJob, input []byte) (delivery.RunAnswer, error) { + id := sdkstep.NewID() + done := m.spaces().Hold(id) + defer m.releaseSaid(id) + defer done() + ws := filepath.Join(m.Work, id) + heads := filepath.Join(ws, "heads") + for _, h := range job.Heads { + if err := m.placeHead(ctx, h, filepath.Join(heads, filepath.FromSlash(h.Repository))); err != nil { + return delivery.RunAnswer{}, fmt.Errorf("the head of %s at %s: %w", h.Repository, h.Commit, err) + } + } + if err := os.MkdirAll(heads, 0o700); err != nil { + return delivery.RunAnswer{}, err + } + file := filepath.Join(ws, "input") + if err := os.WriteFile(file, input, 0o600); err != nil { + return delivery.RunAnswer{}, err + } + run := m.Run + if !m.Inside { + iso := m.isolation(id, false) + iso.ReadOnly = append(iso.ReadOnly, exe) + run = buildstep.Isolated(m.Run, iso) + } + // The heads are bound read-only inside the writable workspace by running the program with them as its read-only + // directory too (a later bind of the same path wins). + r := run(ctx, []string{delivery.RunHeadsEnv + "=" + heads, delivery.RunInputEnv + "=" + file}, exe, job.Args...) + if r.Err != "" && r.Status == 0 { + return delivery.RunAnswer{}, fmt.Errorf("the checking program could not run: %s", r.Err) + } + ans := delivery.RunAnswer{Exit: r.Status, Said: firstLine(r.Err + "\n" + r.Stderr)} + line := strings.TrimSpace(lastLine(r.Stdout)) + switch { + case line == "": + case json.Valid([]byte(line)): + ans.Output = json.RawMessage(line) + default: + ans.Output, _ = json.Marshal(line) + } + return ans, nil +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go b/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go new file mode 100644 index 000000000..c1463d2a8 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go @@ -0,0 +1,105 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/testbus" +) + +// 1921/C3: a run ask put on the Go build seat's check stream is pulled by the module started whole on the real bus and +// answered on its step key .: the program, run shut off over its heads and its input (read from the +// checker's inputs state by digest), answers its last line and exit; a program that dials the machine's network is +// shut off, and the answer says it failed. +func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) { + needTools(t, "the shut-off run", "bash", "bwrap") + nc, err := nats.Connect(testbus.URL(t)) + if err != nil { + t.Fatal(err) + } + defer nc.Close() + js, err := jetstream.New(nc) + if err != nil { + t.Fatal(err) + } + ctx, stop := context.WithCancel(context.Background()) + defer stop() + if err := buildstep.DeclareAskWorkers(ctx, js, delivery.AskGo, 2*time.Minute); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = js.DeleteStream(context.Background(), "SEAT_NODE_BUILD_GO") }) + tcp, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer tcp.Close() + + states := newStates() + m := moduleWith(states, "go1.26.0") + m.Work, m.Cache, m.Home = t.TempDir(), t.TempDir(), t.TempDir() + m.Capacity = func() int { return 1 } + m.Run = StepRunner(m.Home) + for _, c := range m.caches() { + if err := os.MkdirAll(c, 0o700); err != nil { + t.Fatal(err) + } + } + m.PlaceHead = func(_ context.Context, h delivery.RunHead, dir string) error { return os.MkdirAll(dir, 0o700) } + body := `{"v":1}` + m.ReadInput = func(string) (json.RawMessage, error) { return json.RawMessage(body), nil } + + a := runAskOf(t, body) + key := delivery.RunKey(a.Run.Program, delivery.ToolchainUsed{Version: "go1.26.0", Recipe: m.Recipe.Version()}, nil) + exe := m.programPath("programs", key) + os.MkdirAll(filepath.Dir(exe), 0o700) + script := "#!/bin/bash\n" + `echo "start"; exec 3<>/dev/tcp/127.0.0.1/` + strings.Split(tcp.Addr().String(), ":")[1] + " || exit 7\n" + `echo '{"dialed":true}'` + "\n" + if err := os.WriteFile(exe, []byte(script), 0o700); err != nil { + t.Fatal(err) + } + p := m.Puller(fakeSteps(new([]string)), &buildstep.JetStreamAsks{JS: js, Language: delivery.AskGo}) + p.PullWait = 200 * time.Millisecond + go p.Pull(ctx) + + raw, err := delivery.EncodeStepAsk(a) + if err != nil { + t.Fatal(err) + } + subject, _ := delivery.AskSubject(delivery.AskGo, delivery.AskCheck) + if _, err := js.Publish(ctx, subject, raw); err != nil { + t.Fatal(err) + } + stepKey, _ := delivery.AskStepKey("ace", a.ID) + var step delivery.Step + for deadline := time.Now().Add(60 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + states.mu.Lock() + v := states.steps[stepKey] + states.mu.Unlock() + if len(v) > 0 && v[len(v)-1].State != delivery.StepRunning { + step = v[len(v)-1] + break + } + } + if step.State != delivery.StepPassed { + t.Fatalf("the run ask ended %q: %s", step.State, step.Reason) + } + var ans delivery.RunAnswer + if err := json.Unmarshal(step.Result, &ans); err != nil { + t.Fatal(err) + } + if ans.Key != key || ans.Exit != 7 || strings.Contains(string(ans.Output), "dialed") || string(ans.Output) != `"start"` { + t.Fatalf("the answer of a program that dials out = %+v output %s", ans, ans.Output) + } +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run_test.go b/modules/mesh-build-go/cmd/mesh-build-go/run_test.go new file mode 100644 index 000000000..10baa06a8 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/run_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const runCommit = "5555555555555555555555555555555555555555" + +func runAskOf(t *testing.T, body string) delivery.StepAsk { + t.Helper() + in, err := delivery.RunInputOf(InputsState, []byte(body)) + if err != nil { + t.Fatal(err) + } + return delivery.StepAsk{ID: sdkstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "mesh-delivery-checker", At: time.Now(), + Run: &delivery.RunJob{Program: delivery.RunProgram{Repository: "novox/mesh-delivery", Commit: runCommit, Dir: ".", Package: "./cmd/prog"}, + Heads: []delivery.RunHead{{Repository: "novox/mesh-sdk", Commit: runCommit}, {Repository: "novox/mesh-host", Commit: runCommit}}, + Input: in, Args: []string{"-seats"}}} +} + +// 1921/C1: a kept checking program is run over the input checked against its digest and the heads laid under +// MESH_RUN_HEADS at /, and the answer carries its exit and last line; nothing is fetched or built; a +// program that fails, or says no JSON, is an answer; an input that is not the one named is refused; and the program was +// kept under delivery.RunKey, so a second ask finds it. +func TestAKeptProgramIsRunOverItsHeadsAndInput(t *testing.T) { + m := composeModule(t) + body := `{"v":1,"case":"x"}` + m.ReadInput = func(key string) (json.RawMessage, error) { + if key != runAskOf(t, body).Run.Input.Digest { + return nil, nil + } + return json.RawMessage(body), nil + } + m.PlaceHead = func(_ context.Context, h delivery.RunHead, dir string) error { + return os.MkdirAll(dir, 0o700) + } + a := runAskOf(t, body) + key := delivery.RunKey(a.Run.Program, delivery.ToolchainUsed{Version: "go1.26.0", Recipe: m.Recipe.Version()}, nil) + exe := m.programPath("programs", key) + os.MkdirAll(filepath.Dir(exe), 0o700) + script := "#!/bin/sh\n" + + `echo "noise"; echo "args $*" >&2; ` + + `printf '{"input":%s,"heads":"%s"}\n' "$(cat "$MESH_RUN_INPUT")" "$(cd "$MESH_RUN_HEADS" && ls -d */* | tr '\n' ' ')"; exit 3` + "\n" + if err := os.WriteFile(exe, []byte(script), 0o700); err != nil { + t.Fatal(err) + } + fetched := 0 + fetch := func(context.Context, map[string]any) (any, error) { fetched++; return nil, nil } + out, err := m.run(context.Background(), fetch, a) + if err != nil { + t.Fatal(err) + } + ans := out.(delivery.RunAnswer) + if ans.Built || ans.Key != key || fetched != 0 || ans.Exit != 3 || ans.Said != "args -seats" || + string(ans.Output) != `{"input":{"v":1,"case":"x"},"heads":"novox/mesh-host novox/mesh-sdk "}` { + t.Fatalf("answer = %+v (fetched %d) output %s", ans, fetched, ans.Output) + } + if entries, _ := os.ReadDir(m.Work); len(entries) != 0 { + t.Errorf("the run's workspace was left: %v", entries) + } + // An input other than the one named is refused, naming it; an unknown state is refused. + m.ReadInput = func(string) (json.RawMessage, error) { return json.RawMessage(`{"other":1}`), nil } + if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "not the one the ask names") { + t.Errorf("a wrong input: %v", err) + } + a.Run.Input.State = "mesh-delivery-indexer.manifests" + if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "this module reads") { + t.Errorf("another state: %v", err) + } +} + +// 1921/C2 (F2): a compose ask's seats reach the composer: the module decodes and fills the request and writes it again, +// and used to drop the seats. +func TestAComposeAskKeepsItsSeats(t *testing.T) { + m := composeModule(t) + read, digests := indexerHolds(t, `{"module":"demo","version":"1"}`) + m.ReadManifest = read + a := composeAskOf(t, digests[0]) + req := composer.Request{V: composer.Version, Seats: []json.RawMessage{json.RawMessage(`{"seat":"node-x"}`)}, + Cases: []composer.Case{{Name: "change", Manifests: map[string]composer.Manifest{"demo": {Digest: digests[0]}}}}} + a.Compose.Request, _ = json.Marshal(req) + key := delivery.ComposerKey("novox/mesh-sdk", composeCommit, "go", composer.Package, delivery.ToolchainUsed{Version: "go1.26.0", Recipe: m.Recipe.Version()}, nil) + exe := m.composerPath(key) + os.MkdirAll(filepath.Dir(exe), 0o700) + script := "#!/bin/sh\n" + `if grep -q '"seats":\[{"seat":"node-x"}\]' "$2"; then S=true; else S=false; fi; ` + + `echo '{"v":1,"sdk":"stand-in","cases":[{"name":"change","manifests":{"demo":{"accepted":'$S'}}}]}'` + "\n" + os.WriteFile(exe, []byte(script), 0o700) + out, err := m.compose(context.Background(), nil, a) + if err != nil { + t.Fatal(err) + } + var v composer.Verdict + if err := json.Unmarshal(out.(delivery.ComposeAnswer).Verdict, &v); err != nil || !v.Cases[0].Manifests["demo"].Accepted { + t.Fatalf("the composer did not receive the seats: %+v %v", v, err) + } +} diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index ee94e020f..3281af8d3 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -20,6 +20,7 @@ } ], "reads": [ + "mesh-delivery-checker.inputs", "mesh-delivery-checker.kept", "mesh-delivery-indexer.manifests" ],