From 84a76437233704e9cbcf026b3a0d0468b1bb23e1 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 21:19:32 +0200 Subject: [PATCH] Add the repository-identity and trunk-build tests both delivery readers need (issue 1281) A build record names its repository by clone URL and a plan-asked build carries no ref; the indexer and view compared both naively. --- go/delivery/trunk.go | 32 +++++++++++++++++++ go/delivery/trunk_test.go | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 go/delivery/trunk.go create mode 100644 go/delivery/trunk_test.go diff --git a/go/delivery/trunk.go b/go/delivery/trunk.go new file mode 100644 index 0000000..c5c51cb --- /dev/null +++ b/go/delivery/trunk.go @@ -0,0 +1,32 @@ +package delivery + +import "strings" + +// RepositoryName is the repository as the forge names it, "owner/name", from either spelling the mesh holds: the name +// itself, or the clone URL a build record carries ("http://host:port/owner/name.git", with or without ".git" or a +// trailing slash). A string of fewer than two path segments is returned as it is, trimmed (issue 1281). +func RepositoryName(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimRight(s, "/") + s = strings.TrimSuffix(s, ".git") + s = strings.TrimRight(s, "/") + parts := strings.FieldsFunc(s, func(r rune) bool { return r == '/' || r == ':' }) + if len(parts) < 2 { + return s + } + return parts[len(parts)-2] + "/" + parts[len(parts)-1] +} + +// SameRepository is whether two spellings name one repository: the same owner and name, whichever way each is written +// (RepositoryName), compared without regard to case as the forge does. An empty spelling names none (issue 1281). +func SameRepository(a, b string) bool { + a, b = RepositoryName(a), RepositoryName(b) + return a != "" && strings.EqualFold(a, b) +} + +// IsTrunkBuild is whether the record is a successful build of the trunk that recorded its source inputs: state built, and +// ref "main" or none. A build a delivery plan asked after a merge, or rebuild or build --behind, follows the branch +// without naming it, so it carries no ref; a build of any other branch or of a commit names it (issue 1281). +func (r BuildRecord) IsTrunkBuild() bool { + return r.State == "built" && r.SourceInputs != nil && (r.Ref == "" || r.Ref == "main") +} diff --git a/go/delivery/trunk_test.go b/go/delivery/trunk_test.go new file mode 100644 index 0000000..fb84cbf --- /dev/null +++ b/go/delivery/trunk_test.go @@ -0,0 +1,66 @@ +package delivery + +import ( + "encoding/json" + "testing" +) + +func TestSameRepository(t *testing.T) { // 1281/AC1 + const forge = "novox/mesh-catalog" + for _, c := range []struct { + a, b string + same bool + }{ + // the clone URL as a build record carries it (mesh-controller.command builds, 2026-10-19 ~21:20 local) + {forge, "http://novox.internal:20000/novox/mesh-catalog.git", true}, + {forge, "http://novox.internal:20000/novox/mesh-catalog", true}, + {forge, "http://novox.internal:20000/novox/mesh-catalog.git/", true}, + {"http://novox.internal:20000/novox/mesh-catalog.git/", forge, true}, + {forge, "novox/mesh-catalog", true}, + {forge, "Novox/Mesh-Catalog", true}, + {forge, "http://novox.internal:20000/novox/mesh-catalog-x.git", false}, + {forge, "novox/mesh-catalog-x", false}, + {forge, "http://novox.internal:20000/other/mesh-catalog.git", false}, + {forge, "other/mesh-catalog", false}, + {forge, "http://novox.internal:20000/novox/mesh-media-catalog.git", false}, + {forge, "", false}, + {"", "", false}, + } { + if got := SameRepository(c.a, c.b); got != c.same { + t.Errorf("SameRepository(%q, %q) = %v, want %v", c.a, c.b, got, c.same) + } + } + if got := RepositoryName("http://novox.internal:20000/novox/mesh-catalog.git"); got != "novox/mesh-catalog" { + t.Errorf("RepositoryName = %q", got) + } +} + +func TestIsTrunkBuild(t *testing.T) { // 1281/AC2 + inputs := &SourceInputs{Module: "zsh", Tree: "t"} + for _, c := range []struct { + name string + r BuildRecord + want bool + }{ + {"built on main", BuildRecord{State: "built", Ref: "main", SourceInputs: inputs}, true}, + // a build a delivery plan asked after a merge, or rebuild / build --behind, carries no ref (live builds, + // 2026-10-19: zsh build-1792417485811374131, mesh-delivery build-1792435539606203626) + {"built with no ref", BuildRecord{State: "built", SourceInputs: inputs}, true}, + {"another branch", BuildRecord{State: "built", Ref: "feat/x", SourceInputs: inputs}, false}, + {"a commit", BuildRecord{State: "built", Ref: "9efdd87a", SourceInputs: inputs}, false}, + {"failed", BuildRecord{State: "failed", Ref: "main", SourceInputs: inputs}, false}, + {"no state", BuildRecord{Ref: "main", SourceInputs: inputs}, false}, + {"no source inputs", BuildRecord{State: "built", Ref: "main"}, false}, + {"no ref and no source inputs", BuildRecord{State: "built"}, false}, + } { + if got := c.r.IsTrunkBuild(); got != c.want { + t.Errorf("%s: IsTrunkBuild = %v, want %v", c.name, got, c.want) + } + } + // decoded as the controller writes it: the key "ref" is absent + var r BuildRecord + raw := `{"id":"build-1792417485811374131","state":"built","repository":"http://novox.internal:20000/novox/mesh-catalog.git","module":"zsh","source-inputs":{"Module":"zsh","Tree":"t"}}` + if err := json.Unmarshal([]byte(raw), &r); err != nil || !r.IsTrunkBuild() { + t.Errorf("a decoded record with no ref: trunk = %v, err %v", r.IsTrunkBuild(), err) + } +}