From 71afdf3ad23443dc59a281e2b4d13eb97fc382f9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 20:19:21 +0100 Subject: [PATCH] Keep a have held by an interrupted backup and naming nothing out of the node's own novox's postgres stayed held after mesh-host #189: its have, written before that fix, names no build, so the node's own composition still took it in and was refused for its secret reader before the start could back it up again. Only that exact record leaves the node's own; every other have naming nothing keeps main's rule. Completes #189 for a have written before it (issue 1289). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- internal/agent/agent_test.go | 14 ++++++++++---- internal/selfcompose/selfcompose.go | 7 ++++++- internal/selfcompose/selfcompose_test.go | 4 ++++ 3 files changed, 20 insertions(+), 5 deletions(-) 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"})