A check judged the head alone, so two pull requests that each passed could break the trunk together (node-engine #66 on 2026-10-09), and a recheck after the trunk moved judged the same head again. The build seat now merges the head into the trunk's head in its own clone, never pushed, fails a head that does not merge cleanly naming the files, and the verdict names the head, the trunk commit and the commits read beside it, for the forge and mesh-delivery to weigh against the trunk's next move (novox/hq ADR 0323, issue 341).
189 lines
8.3 KiB
Go
189 lines
8.3 KiB
Go
package builder
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// **A merge check judges the merge, not the head** (novox/hq ADR 0323, issue 341): the head merged into the trunk
|
|
// commit in the check's own clone, never pushed; a head that does not merge cleanly fails, naming the files; the
|
|
// verdict names the head, the trunk commit and the commits of the repositories read beside it.
|
|
|
|
// aTrunkAndABranch is a repository whose main moved after the branch was cut: main holds trunkFiles over the
|
|
// common files, the branch headFiles. It answers the repository, the branch's head and main's head.
|
|
func aTrunkAndABranch(t *testing.T, common, trunkFiles, headFiles map[string]string) (string, string, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
git := func(args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
write := func(files map[string]string) {
|
|
for name, body := range files {
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
git("add", "-A")
|
|
}
|
|
git("init", "--quiet", "-b", "main")
|
|
write(common)
|
|
git("commit", "--quiet", "-m", "common")
|
|
git("checkout", "--quiet", "-b", "change")
|
|
write(headFiles)
|
|
git("commit", "--quiet", "-m", "the change")
|
|
head := git("rev-parse", "HEAD")
|
|
git("checkout", "--quiet", "main")
|
|
write(trunkFiles)
|
|
git("commit", "--quiet", "-m", "the trunk moved")
|
|
return dir, head, git("rev-parse", "HEAD")
|
|
}
|
|
|
|
func TestACheckJudgesTheHeadMergedIntoTheTrunk(t *testing.T) {
|
|
// main's script passes only in a tree holding both the trunk's file and the head's: the merge.
|
|
repo, head, trunk := aTrunkAndABranch(t,
|
|
map[string]string{CheckScript: "test -f trunk-only && test -f head-only || { echo 'FAIL: not the merge'; exit 1; }\n"},
|
|
map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"})
|
|
beside, besideHead := aCheckedRepository(t, map[string]string{"README": "the catalogue"})
|
|
registry := aFactsRegistry(t)
|
|
aRuntimeOnThisMachine(t)
|
|
|
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341", Repository: repo, Ref: head, Owner: "novox",
|
|
Repo: "mesh-lab", Number: 1, Base: "main", Toolchain: "golang",
|
|
Beside: map[string]Beside{"mesh-catalog": {Repository: beside, Ref: besideHead}}}, t.TempDir(), registry,
|
|
GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
|
t.Fatalf("the check did not judge the head merged into the trunk: %+v\n%s", v.Repo, v.Report)
|
|
}
|
|
if v.Head != head[:12] || v.Trunk != trunk[:12] {
|
|
t.Fatalf("the verdict names head %q and trunk %q, not %s and %s", v.Head, v.Trunk, head[:12], trunk[:12])
|
|
}
|
|
if v.Beside["mesh-catalog"] != besideHead[:12] {
|
|
t.Fatalf("the verdict names the catalogue read beside it as %q, not %s: %v", v.Beside["mesh-catalog"],
|
|
besideHead[:12], v.Beside)
|
|
}
|
|
if v.Merge == "" || v.Merge == head || v.Merge == trunk {
|
|
t.Fatalf("no merge of the head into the trunk was made for the check: %q", v.Merge)
|
|
}
|
|
}
|
|
|
|
func TestAHeadThatDoesNotMergeCleanlyFailsNamingTheFiles(t *testing.T) {
|
|
repo, head, trunk := aTrunkAndABranch(t,
|
|
map[string]string{"store.go": "package store\n\nconst Declared = 1\n", "other.go": "package store\n"},
|
|
map[string]string{"store.go": "package store\n\nconst Declared = 2\n"},
|
|
map[string]string{"store.go": "package store\n\nconst Declared = 3\n"})
|
|
|
|
// No artifact store and no container runtime: a head that does not merge is judged before either is needed.
|
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-conflict", Repository: repo, Ref: head,
|
|
Owner: "novox", Repo: "mesh-lab", Number: 2, Base: "main", Toolchain: "golang"}, t.TempDir(), "",
|
|
GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("a head that does not merge cleanly was not judged: %v", err)
|
|
}
|
|
want := "does not merge cleanly into trunk " + trunk[:12]
|
|
for name, l := range map[string]*Layer{"gate": v.Gate, "repository": v.Repo} {
|
|
if l == nil || l.Verdict != "fail" || !strings.Contains(l.Summary, want) || !strings.Contains(l.Summary, "store.go") {
|
|
t.Fatalf("its %s layer said %+v, not a failure saying %q and naming store.go", name, l, want)
|
|
}
|
|
}
|
|
if strings.Contains(v.Summary, "other.go") {
|
|
t.Fatalf("a file not in conflict is named: %s", v.Summary)
|
|
}
|
|
if v.Head != head[:12] || v.Trunk != trunk[:12] {
|
|
t.Fatalf("the failure names head %q and trunk %q", v.Head, v.Trunk)
|
|
}
|
|
}
|
|
|
|
// **The merge made for a check is only checked** (ADR 0323 §2): the forge never receives it.
|
|
func TestTheMergeMadeForACheckIsNeverPushed(t *testing.T) {
|
|
repo, head, trunk := aTrunkAndABranch(t, map[string]string{CheckScript: "true\n"},
|
|
map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"})
|
|
registry := aFactsRegistry(t)
|
|
aRuntimeOnThisMachine(t)
|
|
refsBefore := gitOut(t, repo, "for-each-ref")
|
|
|
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-push", Repository: repo, Ref: head, Owner: "novox",
|
|
Repo: "mesh-lab", Number: 3, Base: "main", Toolchain: "golang"}, t.TempDir(), registry, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.Merge == "" {
|
|
t.Fatal("the check made no merge of the head into the trunk")
|
|
}
|
|
if err := exec.Command("git", "-C", repo, "cat-file", "-e", v.Merge+"^{commit}").Run(); err == nil {
|
|
t.Fatalf("the merge %s made for the check reached the forge's repository", v.Merge)
|
|
}
|
|
if after := gitOut(t, repo, "for-each-ref"); after != refsBefore {
|
|
t.Fatalf("the check moved a ref of the forge's repository:\nbefore %s\nafter %s", refsBefore, after)
|
|
}
|
|
if got := gitOut(t, repo, "rev-parse", "main"); got != trunk {
|
|
t.Fatalf("the trunk moved to %s", got)
|
|
}
|
|
}
|
|
|
|
// A delivery group's composed check merges each member's head into its own trunk's head (ADR 0323 §6), and names
|
|
// both for each; a member that does not merge cleanly fails the composed check, naming it.
|
|
func TestAGroupsComposedCheckMergesEachMemberIntoItsTrunk(t *testing.T) {
|
|
repo, head, trunk := aTrunkAndABranch(t, map[string]string{CheckScript: "true\n"},
|
|
map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"})
|
|
member, memberHead, memberTrunk := aTrunkAndABranch(t, map[string]string{"a": "1"},
|
|
map[string]string{"b": "trunk"}, map[string]string{"c": "head"})
|
|
registry := aFactsRegistry(t)
|
|
aRuntimeOnThisMachine(t)
|
|
|
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-group", Repository: repo, Ref: head, Owner: "novox",
|
|
Repo: "mesh-controller", Number: 4, Base: "main", Toolchain: "golang", Group: []GroupHead{{Owner: "novox",
|
|
Repo: "mesh-host", Repository: member, Ref: memberHead, Base: "main"}}}, t.TempDir(), registry,
|
|
GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.Head != head[:12] || v.Trunk != trunk[:12] {
|
|
t.Fatalf("the composed check names its first head %q into %q", v.Head, v.Trunk)
|
|
}
|
|
if len(v.Members) != 1 || v.Members[0].Head != memberHead[:12] || v.Members[0].Trunk != memberTrunk[:12] {
|
|
t.Fatalf("the composed check names its members %+v, not mesh-host %s into %s", v.Members, memberHead[:12],
|
|
memberTrunk[:12])
|
|
}
|
|
|
|
conflicting, conflictHead, _ := aTrunkAndABranch(t, map[string]string{"a": "1"}, map[string]string{"a": "2"},
|
|
map[string]string{"a": "3"})
|
|
v, err = Check(t.Context(), Command, CheckSpec{ID: "check-341-group-conflict", Repository: repo, Ref: head,
|
|
Owner: "novox", Repo: "mesh-controller", Number: 4, Base: "main", Toolchain: "golang", Group: []GroupHead{{
|
|
Owner: "novox", Repo: "mesh-host", Repository: conflicting, Ref: conflictHead, Base: "main"}}}, t.TempDir(),
|
|
registry, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v.Verdict != "fail" || !strings.Contains(v.Summary, "novox/mesh-host") ||
|
|
!strings.Contains(v.Summary, "does not merge cleanly") {
|
|
t.Fatalf("a member that does not merge cleanly was answered %s: %s", v.Verdict, v.Summary)
|
|
}
|
|
}
|
|
|
|
func gitOut(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|