Name what a held release holds, and where it is released
The release-held words said "release them, or leave them held" without the modules, the machines or the mesh MCP server, so the operator could neither tell what waited nor where to act (ADR 0258). The controller's restart needs missed the same suffix. A test now holds every need that opens with a verb only the mesh MCP server performs to name it, so a new kind cannot miss it.
This commit is contained in:
@@ -374,10 +374,7 @@ var plainWordings = map[string]func(conditions.Observation) words{
|
||||
Resolved: "Resolved: " + m + " runs a good build again"}
|
||||
}),
|
||||
"release-held": worded(func(o conditions.Observation) words {
|
||||
return words{Headline: "Updates wait for your release",
|
||||
Needs: "release them, or leave them held.",
|
||||
Explanation: "Some module updates wait for a person to release them, and are not delivered until then.",
|
||||
Resolved: "Resolved: the held updates are released"}
|
||||
return releaseHeldWords(nil, o.Also)
|
||||
}),
|
||||
"facts-stale": worded(func(o conditions.Observation) words {
|
||||
return words{Headline: "Merge checks use outdated facts",
|
||||
@@ -389,21 +386,21 @@ var plainWordings = map[string]func(conditions.Observation) words{
|
||||
// The controller and the core.
|
||||
"controller-deaf": worded(func(o conditions.Observation) words {
|
||||
return words{Headline: "The controller stopped listening",
|
||||
Needs: "restart the controller if this stays.",
|
||||
Needs: "restart the controller if this stays, " + FromMeshMCPServer,
|
||||
Explanation: "The controller, which coordinates the mesh, has taken no messages for minutes while some " +
|
||||
"wait. Changes and repairs do not happen until it recovers.",
|
||||
Resolved: "The controller listens again"}
|
||||
}),
|
||||
"self-check-silent": worded(func(o conditions.Observation) words {
|
||||
return words{Headline: "The mesh's self-check stopped",
|
||||
Needs: "restart the controller if this stays.",
|
||||
Needs: "restart the controller if this stays, " + FromMeshMCPServer,
|
||||
Explanation: "The self-check, which looks over the whole mesh every few minutes, has not finished a run. " +
|
||||
"Problems may go unnoticed until it runs again.",
|
||||
Resolved: "The self-check runs again"}
|
||||
}),
|
||||
"watchdogs-silent": worded(func(o conditions.Observation) words {
|
||||
return words{Headline: "The mesh's watchdogs stopped",
|
||||
Needs: "restart the controller if this stays.",
|
||||
Needs: "restart the controller if this stays, " + FromMeshMCPServer,
|
||||
Explanation: "The watchdogs, which notice when something expected does not happen, have not run, so " +
|
||||
"missed signals are not noticed.",
|
||||
Resolved: "The watchdogs run again"}
|
||||
@@ -670,6 +667,32 @@ func moduleNeeds(node string, rs []inventory.ResourceHealth) string {
|
||||
// authorised (to-be 46 phases 5 and 6).
|
||||
const FromMeshMCPServer = "from the mesh MCP server; this notification cannot do it."
|
||||
|
||||
// releaseHeldWords are the plain words of updates held after a release failed its gate: which modules wait,
|
||||
// on which machines, and where the operator releases them (ADR 0258: a release is not an acknowledgement, so
|
||||
// no notification gives it). Raised with the modules and machines (backlogObservation); the kind's fallback
|
||||
// knows neither.
|
||||
func releaseHeldWords(modules, machines []string) words {
|
||||
what := "Some module updates"
|
||||
headline := "Updates wait for your release"
|
||||
if len(modules) > 0 {
|
||||
what = "Updates of " + namesWords(modules, 3)
|
||||
if h := what + " wait for your release"; len(h) <= conditions.HeadlineMax {
|
||||
headline = h
|
||||
} else if h := fmt.Sprintf("%d module updates wait for your release", len(modules)); len(h) <= conditions.HeadlineMax {
|
||||
headline = h
|
||||
}
|
||||
}
|
||||
where := ""
|
||||
if len(machines) > 0 {
|
||||
where = " on " + namesWords(machines, 4)
|
||||
}
|
||||
return words{Headline: headline,
|
||||
Needs: "release them " + FromMeshMCPServer,
|
||||
Explanation: fmt.Sprintf("%s%s wait for a person to release them, because the last release failed its "+
|
||||
"check. They are not delivered until then, and stay held if you leave them.", what, where),
|
||||
Resolved: "Resolved: the held updates are released"}
|
||||
}
|
||||
|
||||
// reloginNeeds is what an account waiting for its groups needs (ADR 0252).
|
||||
func reloginNeeds(node string) string {
|
||||
return fmt.Sprintf("log out of %s completely and log in again, or restart it.", node)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -222,3 +223,69 @@ func TestDataLossOffersNoSilence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **Updates held after a failed release** (2026-10-08): the popup read "Needs you: release them, or leave
|
||||
// them held." — naming neither what waits nor where it is released. It names the modules and machines, and
|
||||
// the mesh MCP server.
|
||||
func TestUpdatesHeldNameWhatWaitsAndWhereItIsReleased(t *testing.T) {
|
||||
saved := backlogNow
|
||||
t.Cleanup(func() { backlogNow = saved })
|
||||
backlogNow.held = "release-1791457717307061152 failed (failed its gate on g14); what waits is released again by a person"
|
||||
backlogNow.waiting = map[string][]inventory.CarriedMove{
|
||||
"shanks": {{Module: "openrazer"}},
|
||||
"g14": {{Module: "openrazer"}, {Module: "sensors"}},
|
||||
}
|
||||
got := backlogObservation()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("raised %d", len(got))
|
||||
}
|
||||
plainExample(t, got[0], "Updates of openrazer and sensors wait for your release",
|
||||
"Needs you: release them from the mesh MCP server; this notification cannot do it. Updates of openrazer and "+
|
||||
"sensors on g14 and shanks wait for a person to release them, because the last release failed its check. "+
|
||||
"They are not delivered until then, and stay held if you leave them.")
|
||||
}
|
||||
|
||||
// mcpVerb is a need that opens with a verb only the mesh MCP server performs (ADR 0258 §1): release, stop,
|
||||
// start, restart, and a restore.
|
||||
var mcpVerb = regexp.MustCompile(`^(release|stop|start|restart|restore)\b`)
|
||||
|
||||
// **A need no notification can answer says where it is answered** (ADR 0258 §1): every wording whose need
|
||||
// opens with a verb the mesh MCP server performs, and offers no action, ends with FromMeshMCPServer — the
|
||||
// kinds worded here for every subject shape, and those worded where they are raised. A new kind that misses
|
||||
// it fails here; release-held did (2026-10-08).
|
||||
func TestANeedNoNotificationAnswersNamesTheMeshMCPServer(t *testing.T) {
|
||||
check := func(what string, w words) {
|
||||
t.Helper()
|
||||
if w.Needs == "" || len(w.Actions) > 0 || !mcpVerb.MatchString(w.Needs) {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(w.Needs, FromMeshMCPServer) {
|
||||
t.Errorf("%s needs %q without %q", what, w.Needs, FromMeshMCPServer)
|
||||
}
|
||||
}
|
||||
subjects := []conditions.Observation{
|
||||
{Scope: conditions.ScopeMachine, ID: "ace", Machine: "ace"},
|
||||
{Scope: conditions.ScopeMachine, ID: "ace.immich.library", Machine: "ace"},
|
||||
{Scope: conditions.ScopeModule, ID: "openrazer.g14", Machine: "g14"},
|
||||
{Scope: conditions.ScopeDelivery, ID: "novox/hq@055550802096"},
|
||||
{Scope: conditions.ScopeCore, ID: "controller.anchor", Machine: "anchor"},
|
||||
{Scope: conditions.ScopeMesh, ID: "release", Also: []string{"g14"}},
|
||||
}
|
||||
for kind, fn := range plainWordings {
|
||||
for _, s := range subjects {
|
||||
for _, sev := range []conditions.Severity{conditions.Warning, conditions.Urgent} {
|
||||
s.Kind, s.Severity, s.Resolver = kind, sev, conditions.ResolverOperator
|
||||
check(kind+" about "+s.ID, fn(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
check("a walk waiting", words{Needs: waitingNeeds(conditions.Urgent)})
|
||||
check("a module's failed service", words{Needs: moduleNeeds("g14",
|
||||
[]inventory.ResourceHealth{{Kind: link.KindUnit, Target: "openrazer-daemon.service"}})})
|
||||
for _, state := range []string{"held", "ready", "failing"} {
|
||||
_, _, _, needs, actions := stalledWords(stalledLine{ID: "novox/hq@055550802096", State: state, For: "36h"},
|
||||
conditions.Observation{Resolver: conditions.ResolverOperator})
|
||||
check("a delivery "+state, words{Needs: needs, Actions: actions})
|
||||
}
|
||||
check("updates held", releaseHeldWords([]string{"openrazer"}, []string{"g14"}))
|
||||
}
|
||||
|
||||
@@ -710,15 +710,27 @@ func backlogObservation() []conditions.Observation {
|
||||
}
|
||||
n := 0
|
||||
var machines []string
|
||||
seen := map[string]bool{}
|
||||
var modules []string
|
||||
for node, moves := range backlogNow.waiting {
|
||||
n += len(moves)
|
||||
machines = append(machines, node)
|
||||
for _, mv := range moves {
|
||||
if !seen[mv.Module] {
|
||||
seen[mv.Module] = true
|
||||
modules = append(modules, mv.Module)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(machines)
|
||||
return []conditions.Observation{{Scope: conditions.ScopeMesh, ID: "release", Token: "held", Kind: "release-held",
|
||||
Severity: conditions.Warning, Resolver: conditions.ResolverOperator,
|
||||
sort.Strings(modules)
|
||||
o := conditions.Observation{Scope: conditions.ScopeMesh, ID: "release", Token: "held", Kind: "release-held",
|
||||
Severity: conditions.Warning, Resolver: conditions.ResolverOperator, Also: machines,
|
||||
Summary: fmt.Sprintf("%d build move(s) on %s wait for a gate and are not released: %s — `upgrade backlog` lists "+
|
||||
"them, `upgrade release-backlog --why …` releases them", n, strings.Join(machines, ", "), backlogNow.held)}}
|
||||
"them, `upgrade release-backlog --why …` releases them", n, strings.Join(machines, ", "), backlogNow.held)}
|
||||
w := releaseHeldWords(modules, machines)
|
||||
o.Headline, o.Explanation, o.Resolved, o.Needs = w.Headline, w.Explanation, w.Resolved, w.Needs
|
||||
return []conditions.Observation{o}
|
||||
}
|
||||
|
||||
// backlogCommand is `upgrade backlog`, read-only, and `upgrade release-backlog --why`.
|
||||
|
||||
Reference in New Issue
Block a user