From 47ef2644aca144feefb978a83084793722dee540 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 13:48:30 +0000 Subject: [PATCH] Give the build agent a judge cache of its own, and prove a release branch's move puts its verdicts out of date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every merge check's judge is built where no pull request can reach (hq ADR 0358 §6, issue 704): the build agent needs a directory beside its workspace, mounted at the same path, that only judge builds mount. ADR 0358 §5 already holds in the forge's holder; this test keeps it. Co-Authored-By: Claude Opus 5.5 --- modules/build-agent/manifest_test.go | 54 ++++++++++++++++++++++++++++ modules/build-agent/module.json | 14 +++++++- modules/gitea/test/trunk.test.ts | 26 ++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/modules/build-agent/manifest_test.go b/modules/build-agent/manifest_test.go index 163d2d77..06e22fb1 100644 --- a/modules/build-agent/manifest_test.go +++ b/modules/build-agent/manifest_test.go @@ -115,3 +115,57 @@ func TestTheGenesisDocumentIsDeclaredAndIssuedOutside(t *testing.T) { } t.Fatal("the build agent has no environment file") } + +// **The judge cache is the agent's own, beside its workspace and never in it** (novox/hq ADR 0358 §6, issue 704): a +// directory resource of its own, kept as the agent's data, mounted into the agent at the same path as on the host — +// the container runtime resolves a judge build's mounts on the host — and handed to the agent as MESH_JUDGE_CACHE. +func TestTheJudgeCacheIsADirectoryOfItsOwnMountedAtTheSamePath(t *testing.T) { + raw, err := os.ReadFile("module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Data struct { + Own []struct { + ID string `json:"id"` + Path string `json:"path"` + Class string `json:"class"` + } `json:"own"` + } `json:"data"` + Resources []struct { + ID string `json:"id"` + Type string `json:"type"` + Content string `json:"content"` + Volumes []string `json:"volumes"` + } `json:"resources"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + dir, data, env, mounted := false, false, false, false + for _, d := range m.Data.Own { + if d.ID == "judge-cache" && d.Path == "${dir:judge-cache}" && d.Class == "cache" { + data = true + } + } + for _, r := range m.Resources { + switch { + case r.ID == "judge-cache" && r.Type == "directory": + dir = true + case r.ID == "agent-env": + env = strings.Contains(r.Content, "MESH_JUDGE_CACHE=${dir:judge-cache}\n") + case r.ID == "server": + for _, v := range r.Volumes { + if v == "${dir:judge-cache}:${dir:judge-cache}" { + mounted = true + } + if strings.HasPrefix(v, "${dir:workspace}") && strings.Contains(v, "judge-cache") { + t.Errorf("the judge cache is mounted inside the workspace: %s", v) + } + } + } + } + if !dir || !data || !env || !mounted { + t.Fatalf("the judge cache: directory %v, data %v, env %v, mounted at the same path %v", dir, data, env, mounted) + } +} diff --git a/modules/build-agent/module.json b/modules/build-agent/module.json index 9e2a5b22..7cae885c 100644 --- a/modules/build-agent/module.json +++ b/modules/build-agent/module.json @@ -39,6 +39,12 @@ "path": "${dir:workspace}", "class": "cache", "why": "a build's working copy, cloned again for every build" + }, + { + "id": "judge-cache", + "path": "${dir:judge-cache}", + "class": "cache", + "why": "the Go cache every merge check's judge is built with and the release-branch judges kept per head; mounted into judge builds only, never into a check's containers (novox/hq ADR 0358 §6)" } ] }, @@ -54,12 +60,17 @@ "type": "directory", "mode": "0700" }, + { + "id": "judge-cache", + "type": "directory", + "mode": "0700" + }, { "id": "agent-env", "type": "file", "path": "${dir:mesh-state}/build-agent.env", "mode": "0600", - "content": "MESH_BROKER_FILE=/run/mesh/broker\nMESH_NODE=${machine:name}\nMESH_REGISTRY=${bound:artifact-store:at}:${bound:artifact-store:port}\nMESH_PACKAGE_BINDING=/run/mesh/package-registry.json\nMESH_NPM_TOKEN_FILE=/run/mesh/package-registry.secret\nMESH_GENESIS_PACKAGE_REGISTRY=/run/mesh/genesis-package-registry.json\nMESH_WORKSPACE=${dir:workspace}\n" + "content": "MESH_BROKER_FILE=/run/mesh/broker\nMESH_NODE=${machine:name}\nMESH_REGISTRY=${bound:artifact-store:at}:${bound:artifact-store:port}\nMESH_PACKAGE_BINDING=/run/mesh/package-registry.json\nMESH_NPM_TOKEN_FILE=/run/mesh/package-registry.secret\nMESH_GENESIS_PACKAGE_REGISTRY=/run/mesh/genesis-package-registry.json\nMESH_WORKSPACE=${dir:workspace}\nMESH_JUDGE_CACHE=${dir:judge-cache}\n" }, { "id": "server", @@ -72,6 +83,7 @@ "volumes": [ "${dir:mesh-state}:/run/mesh:ro", "${dir:workspace}:${dir:workspace}", + "${dir:judge-cache}:${dir:judge-cache}", "/var/run/docker.sock:/var/run/docker.sock" ], "restart-on": [ diff --git a/modules/gitea/test/trunk.test.ts b/modules/gitea/test/trunk.test.ts index 7e7fdd4b..3eb373a6 100644 --- a/modules/gitea/test/trunk.test.ts +++ b/modules/gitea/test/trunk.test.ts @@ -173,3 +173,29 @@ test("a verdict that landed out of date keeps its account on the pull request", const bare = lateVerdictComment({ commit: HEAD, trunk: TRUNK.slice(0, 12), verdict: "pass" }, null, words); assert.match(bare, /head dfb1415aaaaa merged into trunk a2c2c35bbbbb, pass/); }); + +test("a release branch's move puts every open pull request into it out of date, as a trunk's does (ADR 0358 §5)", async () => { + // novox/hq ADR 0358 §5: for a pull request into a release branch, the release branch takes the trunk's place in ADR + // 0323 — the check merges the head onto the release branch's head and names it as the trunk commit, so the release + // branch's move puts its open verdicts out of date, and leaves those into main alone. + const release = "feat/57-config-and-provisioning"; + const heads: Record = { main: TRUNK, [release]: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }; + const pulls = [{ number: 315, base: release, head: "3".repeat(40) }, { number: 1, base: "main", head: "1".repeat(40) }]; + const { forge, statuses } = aForge(pulls, heads); + const gates = new GateWriter(forge); + const trunks: Record = {}; + await weighTrunks(forge, gates, trunks, "novox", "mesh-controller"); + + heads[release] = MOVED; + assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-controller"), true); + const s = statuses.get("3".repeat(40))!; + assert.equal(s["mesh/merge-gate"].state, "pending"); + assert.match(s["mesh/merge-gate"].description!, /judged against trunk eeeeeeeeeeee, the trunk is now d28ca95ccccc/); + assert.equal(s["mesh/repo-check"].state, "pending"); + assert.equal(statuses.get("1".repeat(40))!["mesh/merge-gate"].state, "success", "a pull request into main is left"); + assert.equal(trunks[`novox/mesh-controller:${release}`], MOVED); + + // The merge tool's rule reads the same: a verdict judged against the release branch's old head is not current. + assert.equal(verdictAtTrunk({ "mesh/merge-gate": { state: "success", + description: "head x merged into trunk eeeeeeeeeeee — pass" } }, MOVED).current, false); +});