package main import ( "encoding/json" "slices" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // failedBuild settles the merge's build of modules/sensors as failed. func conditionOpen(t *testing.T, p inventory.PendingAssignment) bool { t.Helper() _, found, err := conditionsFrom.Get(t.Context(), pendingObservation(p).Key()) if err != nil { t.Fatal(err) } return found } func rowOf(t *testing.T, open *stores, id string) inventory.PendingAssignment { t.Helper() rows, err := open.inventory.Pending(t.Context(), time.Unix(0, 0)) if err != nil { t.Fatal(err) } for _, r := range rows { if r.ID == id { return r } } t.Fatalf("pending assignment %s is not on record", id) return inventory.PendingAssignment{} } // novox/hq issue 325: an assignment of a module the catalogue does not hold says which case it is in — a // build in flight (kept pending), known and not built (said, with build), or unknown (refused, with the // closest names) — and a pending assignment is made when its build registers the module, or ends with why. // aCatalogueMesh is aMesh with one module held from the catalogue, so a merge of it is acted on. // mergeAdding is the catalogue's merge adding one module's directory, acted on as the bus hands it over. // outcome is a build's outcome as the build seat announces it: registered as module, or failed. func outcome(id, dir, commit, module, failed string) link.BuildResult { r := link.BuildResult{ID: id, Repository: "novox/mesh-catalog", Path: dir, Ref: "main", Commit: commit, On: "anchor", Failed: failed, Source: &link.SourceOnSeat{Repository: "novox/mesh-catalog", Seat: "git"}} if failed == "" { r.Module = module r.Manifest, _ = json.Marshal(catalogue.Manifest{Module: module, Version: "1"}) } return r } func assignedTo(t *testing.T, open *stores, node string) []string { t.Helper() got, err := open.inventory.Assigned(t.Context(), node) if err != nil { t.Fatal(err) } return got } func pendingOf(t *testing.T, open *stores) []inventory.PendingAssignment { t.Helper() got, err := open.inventory.Pending(t.Context(), time.Now().Add(-time.Hour)) if err != nil { t.Fatal(err) } return got } // A build in flight: the merge asked for it, the request is kept as the merge's, the assignment is kept // pending and said so with the build, status reads it without settling anything, and the build's outcome // makes it — saying that a push sends it, and nothing more. // **A read settles nothing** (review of #150, point 6): status — and so the board, the summary and the // probe, which compose from the same reading — leaves a pending assignment whose module is registered as it // is; the controller's tick makes it. // The build of a pending assignment fails: it expires with the build's words and nothing is assigned; the // tick raises it as a condition once; it then reads as known and not built; and `unassign` takes it back, // after which the tick clears the condition. Status is not called well while the condition is open, and is // again once it clears — no fixed day of "not well" (review point 6). // The condition also clears when the module is later assigned to the machine. // Known and not built, asked with build: the build is asked from where the last one was, the request kept as // assign's, and the assignment kept pending on the new build. // **build "true" with a pending assignment already waiting** (review point 5): the waiting one is made to // wait for the new build, and no build is asked that nothing waits on. // **A failed ask never reads as in flight** (review point 2): a merge whose ask could not be made keeps the // request as not asked, and assign says the merge could not ask; a request whose asker could not hand it over // or stopped waiting reads the same, unless its outcome was heard. // A plan's tier keeps its requests as the plan's (review point 3). // **A build heard as built and not registered yet is in flight** (review point 4): the outcome is recorded // before the module is registered; for that moment assign keeps the assignment pending. Past the grace it is // recorded and not registered, and the tick ends the pending assignment with that. // Unknown: refused as no module of that name, claiming only what was looked at, with the closest names; build // asks for nothing. // Several modules in one act, one of them not registered: nothing is assigned and nothing kept pending, and // each missing one is said. // unassign withdraws a waiting pending assignment; the build goes on and registers the module assigned // nowhere: a withdrawn row is never claimed. // **A claim is never overridden** (review point 1): a pending assignment being made is claimed // (waiting → applying); an unassign meanwhile is refused and says so, and leaves the claim; making a row // already withdrawn makes nothing. A claim left by a controller that stopped is settled by the tick from what // the mesh holds: back to waiting when the module is not assigned, applied when it is. // A build that says nothing within the bound: the pending assignment expires at the next tick, saying so, // rather than waiting for ever; and a module registered by a process that kept no pending assignment is // assigned at the next tick. // The seat's assign passes build on; unassign takes none. func TestTheSeatsAssignPassesBuild(t *testing.T) { argv, err := argvFor("assign", map[string]any{"node": "g14", "module": "sensors", "build": "true"}) if err != nil || !slices.Equal(argv, []string{"assign", "g14", "sensors", "--build"}) { t.Fatalf("assign with build: %v %v", argv, err) } if _, err := argvFor("unassign", map[string]any{"node": "g14", "module": "sensors", "build": "true"}); err == nil { t.Fatal("unassign took build") } } // The condition's words are plain. func TestAnAssignmentNotMadeIsSaidPlainly(t *testing.T) { o := pendingObservation(inventory.PendingAssignment{Node: "g14", Module: "sensors", Note: "the pending assignment of sensors to g14 expired: build b-1 of modules/sensors failed: boom"}) w := plainWordings[kindPendingEnded](o) if why, ok := conditions.PlainWords(w, "g14"); !ok { t.Fatalf("not plain: %s %+v", why, w) } if w.Headline != "sensors was not put on g14" { t.Fatalf("headline %q", w.Headline) } } // **One row's condition is never another's** (second review, bug A). The sequence: the first pending // assignment's build fails and the tick raises it; the person assigns again with build "true", a second // pending assignment; its build fails too before the next tick. That tick raises the second, and must not // clear it by judging the first answered by the second: each row has its own condition, and a newer row that // itself ended unmade answers nothing. // **A raised row outlives the pruning until its condition clears** (second review, bug B); and a machine's // removal, which takes its rows with it, clears their conditions at the next tick. // **A claim left by a controller that stopped** (second review): the tick settles it from what the mesh // holds — back to waiting when the module is not assigned there, applied when it is — and leaves a fresh // claim alone. // **A waited build whose asker stopped waiting is asked, outcome unknown** (second review): it may still run, // so it reads as in flight until its outcome or its bound, never as not asked; a build never handed over // is not asked.