mesh-config drops an ended assignment as a removed one (issue 1098)

This commit is contained in:
2026-10-18 02:37:28 +02:00
parent 44e7002d3c
commit bdbca4b921
3 changed files with 60 additions and 3 deletions
@@ -551,3 +551,43 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
t.Errorf("read %+v (%v)", call, err)
}
}
// Issue 1098: a value of the assignments bucket with `ended` set, or with a `state`, is not an assignment: it
// resolves nothing and takes the module's resolved values away, as a removed key does.
func TestAnEndedAssignmentIsNotAnAssignment(t *testing.T) {
c, m := aMesh(t)
m.assign(t, "ace", "dunst")
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
t.Fatalf("the assignment moved %+v (%v)", r.Moved, err)
}
for _, ended := range []string{`"ended":"2026-10-18T01:00:00Z"`, `"state":"pending"`} {
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ended+`}`)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("%s moved %+v (%v)", ended, r.Moved, err)
}
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
t.Errorf("%s: the module keeps its resolved values", ended)
}
m.assign(t, "ace", "dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
t.Fatalf("the assignment standing again moved %+v", r.Moved)
}
}
}
// Issue 1098: the fields the controller adds to an assignment change nothing, under the same key.
func TestAnAssignmentCarriesBuildCommitAndRolloutWithoutChangingItsKey(t *testing.T) {
c, m := aMesh(t)
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+
`"build":"b-1","artifact":{"name":"dunst","version":"1.2"},"commit":"abc123","rollout":"r-9",`+
`"since":"2026-10-18T00:00:00Z","ended":"","state":"","future":{"x":1}}`)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 4 {
t.Fatalf("an assignment with the new fields moved %+v (%v)", r.Moved, err)
}
expect(t, m, "ace", "10", SourceDefault)
if r, _ = c.Reconcile(); len(r.Moved) != 0 {
t.Errorf("a quiet pass moved %+v", r.Moved)
}
}
@@ -121,10 +121,24 @@ type Published struct {
Commit string `json:"commit"`
}
// Assignment is one key of the assignments mirror: a module on a node.
// Assignment is one key of the assignments bucket: a module on a node. The value also carries the build, commit and
// rollout that put it there and more; those are read where they are reported, and an unknown field never fails a
// parse. **Ended** and **State** are set on a value that is not a standing assignment (novox/hq ADR 0408 clause 4):
// it reads exactly as a removed key (issue 1098).
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Node string `json:"node"`
Module string `json:"module"`
Ended json.RawMessage `json:"ended,omitempty"`
State json.RawMessage `json:"state,omitempty"`
}
// Standing is whether the value is a standing assignment: neither ended nor in a state.
func (a Assignment) Standing() bool { return !fieldSet(a.Ended) && !fieldSet(a.State) }
// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty.
func fieldSet(f json.RawMessage) bool {
s := strings.TrimSpace(string(f))
return s != "" && s != "null" && s != `""`
}
// rule is the validation a value is held to, from a module's declaration or a predefined key.
@@ -19,6 +19,9 @@ func (c *Config) sources() (Sources, error) {
if err := json.Unmarshal(raw, &a); err != nil || a.Node == "" || a.Module == "" {
return fmt.Errorf("the assignment %s is not one: %s", key, raw)
}
if !a.Standing() {
return nil // ended or pending: read as a removed key (issue 1098)
}
s.Assignments = append(s.Assignments, a)
return nil
}); err != nil {