From 64599db3fa75a4cd636f5e27f5fbdc6702906b44 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 21:23:22 +0200 Subject: [PATCH] Capture the fetch-with-base fixtures from mesh-catalog's code, and prove the checker whole against a real bus with fake build modules (issues 1679, 1287, 1288, 1289, 1678) --- .../mesh-delivery-checker/integration_test.go | 497 ++++++++++++++++++ .../internal/check/examiner_test.go | 14 +- .../internal/check/testdata/captured/README | 10 +- .../testdata/captured/go-fetch-clean.json | 1 + .../testdata/captured/go-fetch-conflict.json | 1 + .../testdata/captured/node-fetch-clean.json | 1 + 6 files changed, 514 insertions(+), 10 deletions(-) create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-clean.json create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-conflict.json create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/node-fetch-clean.json diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go new file mode 100644 index 0000000..f8e50db --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -0,0 +1,497 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the +// controller's user and the checker's own principal .. It carries no per-principal permissions: the grants +// the controller composes from the manifest's state and reads (only the checker writes its buckets) are the controller's, +// and are proved by its own module check, not here. +const busConf = ` +listen: 127.0.0.1:-1 +jetstream { store_dir: %q } +accounts { + MESH { + jetstream: enabled + users = [ + { user: "ctl", password: "pw" } + { user: "n1.mesh-delivery-checker", password: "pw" } + ] + } +} +` + +const ( + repo = "novox/mesh-delivery" + head1 = "1111111111111111111111111111111111111111" + head2 = "2222222222222222222222222222222222222222" + trunk = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + merged = "fedcba9876543210fedcba9876543210fedcba98" // the merge commit of the captured fetch answer + digU = "aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11" + digI = "bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22" +) + +var captured = filepath.Join("..", "..", "internal", "check", "testdata", "captured") + +func readCaptured(t *testing.T, name string) map[string]any { + t.Helper() + raw, err := os.ReadFile(filepath.Join(captured, name+".json")) + if err != nil { + t.Fatal(err) + } + var v map[string]any + if err := json.Unmarshal(raw, &v); err != nil { + t.Fatal(err) + } + return v +} + +// fakeBuildModules serve the tools of mesh-build-go and mesh-build-node as the real ones do (ADR 0469 clause 2): a call on +// mesh.mod..call... answers at once with the step and its key, and the step's end is written to +// the key . of the module's steps bucket. What the steps end with are the results the build modules' own code +// marshalled (internal/check/testdata/captured, README). +type fakeBuildModules struct { + js jetstream.JetStream + ctx context.Context + n atomic.Int64 + conflict atomic.Bool + mu sync.Mutex + calls map[string]int // by `.` + testTool *testing.T +} + +func (f *fakeBuildModules) count(tool string) int { + f.mu.Lock() + defer f.mu.Unlock() + return f.calls[tool] +} + +func (f *fakeBuildModules) result(t *testing.T, tool string, args map[string]any) json.RawMessage { + level, _ := args["level"].(string) + var v any + switch { + case strings.HasSuffix(tool, "_fetch"): + name := "go-fetch-clean" + if f.conflict.Load() { + name = "go-fetch-conflict" + } + fetched := readCaptured(t, name) + fetched["commit"], fetched["base"] = args["commit"], args["base"] + v = fetched + case strings.HasSuffix(tool, "_digest"): + d := map[string]string{"unit": digU, "integration": digI}[level] + v = map[string]any{"level": level, "packages": map[string]string{"fixture/slow": d}} + case strings.HasSuffix(tool, "_test"): + r := readCaptured(t, "go-test-slow") + r["level"] = level + v = r + } + data, _ := json.Marshal(v) + return data +} + +func (f *fakeBuildModules) serve(t *testing.T, nc *nats.Conn, module string) { + steps, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: delivery.StepsBucket(module), History: 1}) + if err != nil { + t.Fatal(err) + } + sub, err := nc.Subscribe("mesh.mod."+module+".call.>", func(m *nats.Msg) { + tokens := strings.Split(m.Subject, ".") // mesh mod call + tool, node := tokens[4], tokens[5] + f.mu.Lock() + f.calls[module+"."+tool]++ + f.mu.Unlock() + var args map[string]any + _ = json.Unmarshal(m.Data, &args) + id := fmt.Sprintf("s%d", f.n.Add(1)) + key, _ := delivery.StepKey(node, id) + put := func(s delivery.Step) { + data, err := delivery.EncodeStep(s) + if err == nil { + _, err = steps.Put(f.ctx, key, data) + } + if err != nil { + t.Errorf("writing step %s: %v", key, err) + } + } + step := delivery.Step{ID: id, Tool: tool, Node: node, State: delivery.StepRunning, Started: time.Now().UTC()} + put(step) + answer, _ := json.Marshal(map[string]any{"result": map[string]any{"step": id, "key": key, "state": "running"}, "error": "", "node": node}) + _ = m.Respond(answer) + go func() { + time.Sleep(20 * time.Millisecond) + step.Ended = time.Now().UTC() + step.State, step.Result = delivery.StepPassed, f.result(t, tool, args) + put(step) + }() + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sub.Unsubscribe() }) +} + +type bed struct { + ctx context.Context + js jetstream.JetStream + buckets map[string]jetstream.KeyValue + tools map[string]jetstream.KeyValue + fake *fakeBuildModules + srvURL string + announce []string + mu sync.Mutex +} + +// startBed starts the bus, the buckets the controller makes from the checker's and the other parts' manifests, the build +// modules' streams and the checker's durable consumers on them, one node's toolchains, the gate's source naming the trunk, and +// the fake build modules. It runs no checker. +func startBed(t *testing.T) *bed { + dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + conf := filepath.Join(dir, "bus.conf") + if err := os.WriteFile(conf, []byte(fmt.Sprintf(busConf, filepath.Join(dir, "js"))), 0o600); err != nil { + t.Fatal(err) + } + opts, err := server.ProcessConfigFile(conf) + if err != nil { + t.Fatal(err) + } + opts.NoLog, opts.NoSigs = true, true + srv, err := server.NewServer(opts) + if err != nil { + t.Fatal(err) + } + go srv.Start() + if !srv.ReadyForConnections(10 * time.Second) { + t.Fatal("the bus did not come up") + } + t.Cleanup(srv.Shutdown) + ctl, err := nats.Connect(srv.ClientURL(), nats.UserInfo("ctl", "pw")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(ctl.Close) + js, err := jetstream.New(ctl) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + t.Cleanup(cancel) + b := &bed{ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()} + for module, stream := range toolchainStreams { + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: stream, Subjects: []string{"mesh.stream." + module + ".event.toolchain.reported"}}); err != nil { + t.Fatal(err) + } + if _, err := js.CreateOrUpdateConsumer(ctx, stream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil { + t.Fatal(err) + } + kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1}) + if err != nil { + t.Fatal(err) + } + b.tools[module] = kv + } + // The checker's stream of events, which the indexer and the forge bridge hear. + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{"mesh.stream.mesh-delivery-checker.event.>"}}); err != nil { + t.Fatal(err) + } + for name, history := range map[string]int{delivery.CheckerVerdictsBucket: delivery.CheckerVerdictsHistory, delivery.CheckerKeptBucket: delivery.CheckerKeptHistory, + delivery.CheckerOwedBucket: delivery.CheckerOwedHistory, delivery.PreviewsBucket: delivery.PreviewsHistory, delivery.CandidatesBucket: delivery.CandidatesHistory, + delivery.ModulesBucket: delivery.ModulesHistory, delivery.ManifestsBucket: delivery.ManifestsHistory, delivery.SourceBucket: delivery.SourceHistory, + delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory} { + kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)}) + if err != nil { + t.Fatal(err) + } + b.buckets[name] = kv + } + b.setOnline(t, "n1", true) + for _, module := range []string{"mesh-build-go", "mesh-build-node"} { + b.setHealth(t, "n1", module) + } + b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}} + b.fake.serve(t, ctl, "mesh-build-go") + b.fake.serve(t, ctl, "mesh-build-node") + b.report(t, "mesh-build-go", "n1", "go", "go1.27.1", "1") + b.report(t, "mesh-build-node", "n1", "typescript", "tsc5", "1") + b.setTrunk(t, map[string]string{repo: trunk}) + return b +} + +func (b *bed) setTrunk(t *testing.T, heads map[string]string) { + t.Helper() + data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[delivery.SourceBucket].Put(b.ctx, delivery.SourceMainKey, data); err != nil { + t.Fatal(err) + } +} + +func (b *bed) setOnline(t *testing.T, node string, online bool) { + t.Helper() + data, _ := delivery.EncodePresence(delivery.Presence{Online: online}) + if _, err := b.buckets[delivery.PresenceBucket].Put(b.ctx, node, data); err != nil { + t.Fatal(err) + } +} + +func (b *bed) setHealth(t *testing.T, node, module string) { + t.Helper() + data, err := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + if err != nil { + t.Fatal(err) + } + key, _ := delivery.HaveKey(node, module) + if _, err := b.buckets[delivery.HaveBucket].Put(b.ctx, key, data); err != nil { + t.Fatal(err) + } +} + +func (b *bed) report(t *testing.T, module, node, language, version, recipe string) { + t.Helper() + data, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()}) + if err != nil { + t.Fatal(err) + } + key, _ := delivery.ToolchainKey(node, language) + if _, err := b.tools[module].Put(b.ctx, key, data); err != nil { + t.Fatal(err) + } + if _, err := b.js.Publish(b.ctx, "mesh.stream."+module+".event.toolchain.reported", []byte(`{}`)); err != nil { + t.Fatal(err) + } +} + +// candidate writes candidates. as the indexer does (the SDK's EncodeCandidate is the indexer's own encoder). +func (b *bed) candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) string { + t.Helper() + heads := make([]delivery.Head, len(pulls)) + for i, p := range pulls { + heads[i] = p.Head + } + hash, err := delivery.HeadsHash(heads) + if err != nil { + t.Fatal(err) + } + data, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + if err != nil { + t.Fatal(err) + } + key, _ := delivery.CandidatesKey(group) + if _, err := b.buckets[delivery.CandidatesBucket].Put(b.ctx, key, data); err != nil { + t.Fatal(err) + } + vk, _ := delivery.CheckerVerdictKey(group, hash) + return vk +} + +func pull(repository string, number int, head string) delivery.CandidatePull { + return delivery.CandidatePull{Head: delivery.Head{Repository: repository, Number: number, Head: head}, Modules: []string{}} +} + +// start runs the program as the checker of a node. +func (b *bed) start(t *testing.T, env map[string]string) { + t.Helper() + broker := filepath.Join(t.TempDir(), "broker") + cred, _ := json.Marshal(credential{URL: b.srvURL, Node: "n1", Module: "mesh-delivery-checker", User: "n1.mesh-delivery-checker", Password: "pw"}) + if err := os.WriteFile(broker, cred, 0o600); err != nil { + t.Fatal(err) + } + e := map[string]string{"MESH_BROKER_FILE": broker} + for k, v := range env { + e[k] = v + } + runCtx, stop := context.WithCancel(b.ctx) + done := make(chan error, 1) + go func() { done <- run(runCtx, func(k string) string { return e[k] }, io.Discard) }() + t.Cleanup(func() { stop(); <-done }) +} + +// verdict reads the verdict key until pred holds of it, within 20 s. +func (b *bed) verdict(t *testing.T, key string, pred func(delivery.Verdict) bool) delivery.Verdict { + t.Helper() + var last delivery.Verdict + for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + e, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, key) + if err != nil { + continue + } + if last, err = delivery.DecodeVerdict(e.Value()); err == nil && pred(last) { + return last + } + } + t.Fatalf("verdicts.%s = %+v within 20 s", key, last) + return last +} + +func ended(v delivery.Verdict) bool { return v.State != delivery.VerdictRunning } + +// owed reads the status the checker owes on a head. +func (b *bed) owed(t *testing.T, number int, head, state string) delivery.Owed { + t.Helper() + key, err := delivery.CheckerOwedKey(repo, number, head) + if err != nil { + t.Fatal(err) + } + var last delivery.Owed + for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + e, err := b.buckets[delivery.CheckerOwedBucket].Get(b.ctx, key) + if err != nil { + continue + } + if last, err = delivery.DecodeCheckerOwed(e.Value()); err == nil && last.State == state { + return last + } + } + t.Fatalf("owed.%s = %+v within 20 s, want the state %s", key, last, state) + return last +} + +// TestCheckerJudgesACandidateAndOwesItsStatus: a candidate the indexer wrote, one pull request of mesh-delivery, is fetched +// merged onto the trunk the gate names, digested and tested at both levels on the node, kept, previewed; the verdict passes +// with the counts of both levels and the composition not run, and the status mesh/check is owed on the head as success. +func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300/AC5, 1445/AC1 + b := startBed(t) + key := b.candidate(t, "feat/x", pull(repo, 7, head1)) + b.start(t, nil) + v := b.verdict(t, key, ended) + if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 || v.Trunk[repo] != trunk { + t.Fatalf("verdict = %+v", v) + } + if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun || len(v.Machines) != 1 || v.Machines[0] != "n1" { + t.Errorf("composition %+v machines %v", v.Composition, v.Machines) + } + o := b.owed(t, 7, head1, "success") + if o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 2 run, 0 kept") { + t.Errorf("owed = %+v", o) + } + for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 1, "mesh-build-go.build_go_digest": 2, "mesh-build-go.build_go_test": 2} { + if got := b.fake.count(tool); got != want { + t.Errorf("%s called %d times, want %d", tool, got, want) + } + } + // What passed is kept once under each level's digest. + for _, k := range []string{"unit." + digU, "integration." + digI} { + if _, err := b.buckets[delivery.CheckerKeptBucket].Get(b.ctx, k); err != nil { + t.Errorf("kept.%s: %v", k, err) + } + } + // The preview of the head merged onto the trunk, with the tree and the manifest digest the fetch named. + pk, _ := delivery.PreviewKey(repo, head1, trunk) + e, err := b.buckets[delivery.PreviewsBucket].Get(b.ctx, pk) + if err != nil { + t.Fatalf("previews.%s: %v", pk, err) + } + pv, err := delivery.DecodePreview(e.Value()) + if err != nil || pv.Merge != merged || pv.Modules["modules/m"].Tree != "1111111111111111111111111111111111111111" { + t.Errorf("preview = %+v, %v", pv, err) + } +} + +// TestACheckOfEqualInputsRunsNothing: a second candidate (another head, the same digests) is checked with every package +// taken from the first's kept verdicts: no test step runs and no second kept key appears. +func TestACheckOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 1288/AC6 + b := startBed(t) + first := b.candidate(t, "feat/x", pull(repo, 7, head1)) + b.start(t, nil) + b.verdict(t, first, ended) + tests, kept := b.fake.count("mesh-build-go.build_go_test"), keptCount(t, b) + second := b.candidate(t, "feat/y", pull(repo, 8, head2)) + v := b.verdict(t, second, ended) + if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 0 || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Run != 0 { + t.Fatalf("verdict = %+v, want everything taken from the kept verdicts", v) + } + if got := b.fake.count("mesh-build-go.build_go_test"); got != tests { + t.Errorf("build_go_test called %d times, was %d: a test ran for equal inputs", got, tests) + } + if got := keptCount(t, b); got != kept { + t.Errorf("%d kept keys, was %d", got, kept) + } +} + +func keptCount(t *testing.T, b *bed) int { + t.Helper() + keys, err := b.buckets[delivery.CheckerKeptBucket].Keys(b.ctx) + if err != nil { + return 0 + } + return len(keys) +} + +// TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest: the fetch answers the files in conflict; the verdict fails naming +// them, no test or digest step is run, no preview is written and the status is owed as failure. +func TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest(t *testing.T) { // 1289/AC4 + b := startBed(t) + b.fake.conflict.Store(true) + key := b.candidate(t, "feat/x", pull(repo, 7, head1)) + b.start(t, nil) + v := b.verdict(t, key, ended) + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "files in conflict: a.go, sub/b.go") { + t.Fatalf("verdict = %+v", v) + } + if o := b.owed(t, 7, head1, "failure"); !strings.HasPrefix(o.Text, "failed: n1: ") || !strings.Contains(o.Text, "files in conflict: a.go, sub/b.go") { + t.Errorf("owed = %+v", o) + } + if n := b.fake.count("mesh-build-go.build_go_digest") + b.fake.count("mesh-build-go.build_go_test"); n != 0 { + t.Errorf("%d digest and test steps ran after a conflict", n) + } + if keys, _ := b.buckets[delivery.PreviewsBucket].Keys(b.ctx); len(keys) != 0 { + t.Errorf("previews %v for a head that does not merge", keys) + } +} + +// TestAnHqPullRequestGetsNoVerdictAndAnotherDoes: hq's pull request is judged by no one (ADR 0484 clause 8), the other one in +// the same run is. +func TestAnHqPullRequestGetsNoVerdictAndAnotherDoes(t *testing.T) { // ADR 0484 clause 8 + b := startBed(t) + hq := b.candidate(t, "feat/hq", pull("novox/hq", 3, head1)) + other := b.candidate(t, "feat/x", pull(repo, 7, head2)) + b.start(t, nil) + b.verdict(t, other, ended) + if _, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, hq); err == nil { + t.Error("hq's pull request has a verdict") + } +} + +// TestACheckLeftRunningIsTakenOverWhenTheCheckerStarts: the verdict key holds running, left by a checker that stopped; the +// started checker judges it again and ends it (ADR 0478 clause 6, as the builder's). +func TestACheckLeftRunningIsTakenOverWhenTheCheckerStarts(t *testing.T) { // N6/AC2 + b := startBed(t) + key := b.candidate(t, "feat/x", pull(repo, 7, head1)) + group, heads, _ := delivery.ParseCheckerVerdictKey(key) + left, err := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, Trunk: map[string]string{repo: trunk}, State: delivery.VerdictRunning}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[delivery.CheckerVerdictsBucket].Create(b.ctx, key, left); err != nil { + t.Fatal(err) + } + b.start(t, nil) + if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass { + t.Errorf("verdict = %+v", v) + } +} diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index ba1109d..39e1ac8 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -116,8 +116,8 @@ type bench struct { // the captured slow fixture's result at both levels, on shanks. func newBench(t testing.TB) *bench { b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV()} - b.fetch = Fetched{Workspace: "ws1", Commit: head1, Modules: []string{"."}, Base: trunkA, Merge: "9999999999999999999999999999999999999999", - Changed: []string{"README.md"}} + b.fetch = captured[Fetched](t, "go-fetch-clean") + b.fetch.Modules, b.fetch.Changed, b.fetch.Trees = []string{"."}, []string{"README.md"}, nil one := captured[Digested](t, "go-digest") b.digest = map[string]Digested{ delivery.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}}, @@ -268,9 +268,9 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3 func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1292/AC1 b := newBench(t) b.ex.Modules = langs{repoA: {buildrun.NodeBuildModule}} - b.fetch.Modules = []string{"modules/ui"} + b.fetch = captured[Fetched](t, "node-fetch-clean") b.test = func(c stepCall) (any, error) { - return Tested{Level: c.args["level"].(string), Module: "modules/ui", Passed: true, Elapsed: 0.4, Bound: 5}, nil + return Tested{Level: c.args["level"].(string), Module: "modules/m", Passed: true, Elapsed: 0.4, Bound: 5}, nil } cs := caseOf(t, pullOf(repoA, 7, head1)) for i := 0; i < 2; i++ { @@ -338,10 +338,10 @@ func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 12 func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T) { // 1289/AC4 b := newBench(t) - b.fetch = Fetched{Workspace: "ws1", Commit: head1, Base: trunkA, Conflicts: []string{"go.mod", "internal/x.go"}} + b.fetch = captured[Fetched](t, "go-fetch-conflict") out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) f := out.Verdict.Failed - if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: go.mod, internal/x.go") { + if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") { t.Fatalf("verdict = %+v", out.Verdict) } if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 { @@ -483,7 +483,7 @@ func TestACleanMergeWritesItsPreviewWithTheTreesManifestsAndLocks(t *testing.T) func TestAHeadThatDoesNotMergeWritesNoPreview(t *testing.T) { // 1445/AC5 b := newBench(t) - b.fetch = Fetched{Workspace: "ws1", Commit: head1, Base: trunkA, Conflicts: []string{"go.mod"}} + b.fetch = captured[Fetched](t, "go-fetch-conflict") b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) if len(b.prev.vals) != 0 { t.Errorf("previews %v written for a head that does not merge", b.prev.vals) diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/README b/modules/mesh-delivery-checker/internal/check/testdata/captured/README index e39f215..b8023fe 100644 --- a/modules/mesh-delivery-checker/internal/check/testdata/captured/README +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/README @@ -4,6 +4,10 @@ test beside the module's test_test.go and digest_test.go (workspaceFor + runUnit slow and empty, and digestFixture + digests on testdata/repos/digest) at mesh-catalog origin/main cdbdb474, captured on 2026-10-21 for issue 1679 and not kept there. The fixtures' package names are the build module's own (fixture/failing/bad...). -The fields a fetch with a base adds (base, merge, conflicts, changed, trees) are the contract the lead relayed to the -mesh-catalog agent (ADR 0484 clause 3, 2026-10-21); mesh-catalog's fetch step is not on main yet, so the fetch fixtures in -the tests are written from that contract and are to be replaced by a capture once its fetch_test can marshal one. +go-fetch-clean.json and go-fetch-conflict.json are what mesh-build-go's fetch step ends with (FetchResult, cmd/mesh-build-go +fetch.go and preview.go) for a fetch with a base, the Result bytes of the step in mesh-catalog's own tests +TestFetchWithABaseMergesAndAnswersThePreview and TestFetchWithAConflictEndsPassedAndNamesTheFiles (preview_test.go), copied +into a throwaway test that wrote them out, at mesh-catalog feat/r4c5-checker d5804927 (the git commands are the test's fake, +so the shas are its constants; the shape and the field names are the module's own marshalling). node-fetch-clean.json is the +FetchResult json.Marshal of mesh-build-node's fetch (fetch.go, preview.go) in its test TestFetchWithABaseMergesAndAnswersThePreview +(cmd/mesh-build-node/preview_test.go) at the same commit. Captured on 2026-10-21 for issue 1679. diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-clean.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-clean.json new file mode 100644 index 0000000..ce2e947 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-clean.json @@ -0,0 +1 @@ +{"workspace":"mvii1iwu5414e4","commit":"0123456789abcdef0123456789abcdef01234567","modules":["."],"base":"abcdef0123456789abcdef0123456789abcdef01","merge":"fedcba9876543210fedcba9876543210fedcba98","changed":["a.go","modules/m/main.go"],"trees":{"modules/m":{"tree":"1111111111111111111111111111111111111111","manifest":"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356","locks":{"go.sum":"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9"}}}} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-conflict.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-conflict.json new file mode 100644 index 0000000..e1d8a99 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/go-fetch-conflict.json @@ -0,0 +1 @@ +{"workspace":"mvii1iwz5050eb","commit":"0123456789abcdef0123456789abcdef01234567","modules":["."],"base":"abcdef0123456789abcdef0123456789abcdef01","conflicts":["a.go","sub/b.go"]} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/node-fetch-clean.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/node-fetch-clean.json new file mode 100644 index 0000000..4c8eedb --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/node-fetch-clean.json @@ -0,0 +1 @@ +{"workspace":"mvii1sxt979f67","commit":"0123456789abcdef0123456789abcdef01234567","modules":["modules/m"],"installed":[],"refused":[],"base":"abcdef0123456789abcdef0123456789abcdef01","merge":"0123456789abcdef0123456789abcdef01234567","changed":["modules/m/package.json"],"trees":{"modules/m":{"tree":"0123456789abcdef0123456789abcdef01234567","manifest":"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef"}}}