Count a build record without a state as the outcome it is (issue 1281): the controller never stores state built

This commit is contained in:
2026-10-19 22:39:29 +00:00
parent 6079ddfe84
commit 58373d65b4
5 changed files with 24 additions and 17 deletions
+1
View File
@@ -11,6 +11,7 @@ import "time"
type BuildRecord struct {
ID string `json:"id"`
State string `json:"state,omitempty"`
Failed string `json:"failed,omitempty"`
Repository string `json:"repository"`
Ref string `json:"ref,omitempty"`
Module string `json:"module,omitempty"`
+9 -4
View File
@@ -11,7 +11,8 @@ import (
// internal/inventory/chain_requests.go:19, the module's directory in its repository (Build.Path, internal/chain/records.go:100).
// The fixture is a record the controller's own encoder wrote (json.Marshal of a chain.Build, mesh-controller
// internal/chain, a throwaway test run once and deleted) with build-agent's build-1792395447088936520 values, plus a
// manifest-digest and one made artifact, which a BuildRecord does not carry.
// manifest-digest and one made artifact (shape: mesh-controller internal/chain/records.go:91-117 Build, json tags, at commit
// 1f708e2d8d72e2c27dbeaba02f14c128d700eada; an outcome has no state key and a build asked after a merge no ref, records.go:57-59), which a BuildRecord does not carry.
func loadBuildFixture(t *testing.T) []byte {
t.Helper()
raw, err := os.ReadFile("testdata/build-record-build-agent.json")
@@ -29,10 +30,11 @@ func TestBuildRecordDecodes(t *testing.T) { // 1223/AC1
for _, c := range []struct{ name, got, want string }{
{"id", r.ID, "build-1792395447088936520"},
{"module", r.Module, "build-agent"},
{"repository", r.Repository, "novox/mesh-controller"},
{"ref", r.Ref, "main"},
{"repository", r.Repository, "http://novox.internal:20000/novox/mesh-controller.git"},
{"ref", r.Ref, ""},
{"commit", r.Commit, "7a74cee83f88e6585a5511c3cc53bdf0f5303d20"},
{"state", r.State, "built"},
{"state", r.State, ""},
{"failed", r.Failed, ""},
{"source-fingerprint", r.SourceFingerprint, "src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7"},
} {
if c.got != c.want {
@@ -42,6 +44,9 @@ func TestBuildRecordDecodes(t *testing.T) { // 1223/AC1
if want := time.Date(2026, 10, 19, 9, 38, 0, 0, time.UTC); !r.At.Equal(want) {
t.Errorf("at = %v, want %v", r.At, want)
}
if !r.IsTrunkBuild() { // 1281/AC7
t.Error("the fixture, a record as the controller writes it, is not a trunk build")
}
if r.SourceInputs == nil {
t.Fatal("source-inputs missing")
}
+1 -1
View File
@@ -1 +1 @@
{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"}
{"id":"build-1792395447088936520","repository":"http://novox.internal:20000/novox/mesh-controller.git","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"}
+3 -3
View File
@@ -24,9 +24,9 @@ func SameRepository(a, b string) bool {
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
// IsTrunkBuild is whether the record is a successful build of the trunk that recorded its source inputs: an outcome
// (the controller writes no state on one; state asked is a request, records.go:57-59), not failed, 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")
return r.State == "" && r.Failed == "" && r.SourceInputs != nil && (r.Ref == "" || r.Ref == "main")
}
+10 -9
View File
@@ -42,16 +42,17 @@ func TestIsTrunkBuild(t *testing.T) { // 1281/AC2
r BuildRecord
want bool
}{
{"built on main", BuildRecord{State: "built", Ref: "main", SourceInputs: inputs}, true},
{"built on main", BuildRecord{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},
{"built with no ref", BuildRecord{SourceInputs: inputs}, true},
{"another branch", BuildRecord{Ref: "feat/x", SourceInputs: inputs}, false},
{"a commit", BuildRecord{Ref: "9efdd87a", SourceInputs: inputs}, false},
{"asked", BuildRecord{State: "asked", SourceInputs: inputs}, false},
{"answered, state built", BuildRecord{State: "built", SourceInputs: inputs}, false},
{"failed", BuildRecord{Failed: "boom", Ref: "main", SourceInputs: inputs}, false},
{"no source inputs", BuildRecord{Ref: "main"}, false},
{"no ref and no source inputs", BuildRecord{}, false},
} {
if got := c.r.IsTrunkBuild(); got != c.want {
t.Errorf("%s: IsTrunkBuild = %v, want %v", c.name, got, c.want)
@@ -59,7 +60,7 @@ func TestIsTrunkBuild(t *testing.T) { // 1281/AC2
}
// 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"}}`
raw := `{"id":"build-1792417485811374131","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)
}