diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 6ed291d..e6c128a 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -618,17 +618,23 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { detail, backup string backups, applied int phase delivery.Phase + namesNothing bool // the have names no fp and no composition }{ - {BackupInterrupted, delivery.BackupAgain, 1, 1, delivery.PhaseRunning}, - {BackupInterrupted, "", 1, 1, delivery.PhaseRunning}, - {"restic: the repository is locked", delivery.BackupAgain, 0, 0, delivery.PhaseHeld}, + {BackupInterrupted, delivery.BackupAgain, 1, 1, delivery.PhaseRunning, false}, + // novox's postgres: its held have, written before mesh-host #189, names nothing it runs. + {BackupInterrupted, "", 1, 1, delivery.PhaseRunning, true}, + {"restic: the repository is locked", delivery.BackupAgain, 0, 0, delivery.PhaseHeld, false}, } { r := newBed(t) r.comps.before(ck1, craw1) r.comps.before(ck2, craw2) wk, wraw := wantFor(t, "db", fp2, dig2, row.backup) r.wants.before(wk, wraw) - r.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseHeld), Detail: row.detail}} + held := Have{Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseHeld), Detail: row.detail} + if row.namesNothing { + held.FP, held.Composed = "", "" + } + r.haves.stored = map[string]Have{"db": held} stopR := r.run() if row.applied > 0 { eventually(t, row.detail+": not applied after its backup", func() bool { return len(r.appliedList()) == row.applied }) diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 28d4867..bb34508 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -618,7 +618,12 @@ func (c *Composer) takeModuleHave(u agent.Update) bool { // A have that names nothing it runs, failed (the node could not compose a module it never ran: NotComposed), is // no module run inside the node's own composition: taken as one, it was composed from its manifest past its // build (issue 1126, review round 1 of the builds). - runsNothing := h.Phase == delivery.PhaseFailed && h.FP == "" && h.Composed == "" + // Nor is one held by a backup the node agent's own stop interrupted that names nothing (a have written before + // mesh-host #189, novox's postgres 2026-10-31): taken as one, it was composed from its manifest and refused + // for its secret reader before the start backed it up again. Any other have naming nothing keeps the rule + // (waiting for a yes inside the node's own stays in). + interrupted := h.Phase == delivery.PhaseHeld && h.Detail == agent.BackupInterrupted + runsNothing := (h.Phase == delivery.PhaseFailed || interrupted) && h.FP == "" && h.Composed == "" in = h.Phase != delivery.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) } } diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 29a4a39..c1413ba 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1760,6 +1760,10 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false}, {"held today", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseHeld, Health: delivery.HealthWaits, Detail: "held: a required config value is unset"}, true, true}, + // novox's postgres (2026-10-31), its have written before mesh-host #189: held by a backup the node agent's stop + // interrupted and naming nothing; counted in, it was composed from its manifest and refused for its secret reader. + {"held by an interrupted backup, naming nothing", delivery.Have{Phase: delivery.PhaseHeld, Health: delivery.HealthWaits, + Detail: agent.BackupInterrupted}, false, true}, } { t.Run(row.name, func(t *testing.T) { c := New(Env{Node: "n1"})