From ece79788a3138e18052a44f9f00cd90ad668f48b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 00:21:10 +0100 Subject: [PATCH] A-112: the node-engine reads recovery-wait-minutes from a file its manifest declares, 90 when missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest declares the file recovery-wait (content ${config:recovery-wait-minutes}); the node-engine reads it at its start and does not call stdio.Config, which needs the runtime that launches a bundle. Builds: design 47 G §633, A-112 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- cmd/mesh-host/recovery.go | 18 ++++++++++++------ cmd/mesh-host/recovery_integration_test.go | 17 +++++++++++++++++ module.json | 7 +++++++ packaging/operator_keys_test.go | 3 +++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/cmd/mesh-host/recovery.go b/cmd/mesh-host/recovery.go index e7762514..9ce2acde 100644 --- a/cmd/mesh-host/recovery.go +++ b/cmd/mesh-host/recovery.go @@ -3,10 +3,12 @@ package main import ( "context" "encoding/json" + "os" + "strconv" + "strings" "time" "git.novox.be/novox/mesh-contracts/go/seats/meshrecovery" - stdio "git.novox.be/novox/mesh-sdk/go" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/link" @@ -68,15 +70,19 @@ func recoveryEnv(session *link.Session, composer *selfcompose.Composer) *agent.R } } -// recoveryWaitMinutes is the manifest's config key recovery-wait-minutes, read through mesh-sdk's stdio.Config; its default is -// the design's 90 (design 47 G §633). +// recoveryWaitFile is the file the manifest declares with the content ${config:recovery-wait-minutes} (design 47 G §633, A-112): the +// node-engine is no bundle under a runtime, so it reads the setting from its file at its start, as the builder does (A-71). +var recoveryWaitFile = "/var/lib/mesh-host/recovery-wait-minutes" + +// recoveryWaitMinutes is the bound of the wait for the recovery module, in minutes: the file's number, the design's 90 when the +// file is missing, empty or not a positive number (design 47 G §554, A-112). func recoveryWaitMinutes() int { - raw, err := stdio.Config("recovery-wait-minutes") + raw, err := os.ReadFile(recoveryWaitFile) if err != nil { return agent.RecoveryWaitMinutes } - var n int - if json.Unmarshal(raw, &n) != nil || n <= 0 { + n, err := strconv.Atoi(strings.TrimSpace(string(raw))) + if err != nil || n <= 0 { return agent.RecoveryWaitMinutes } return n diff --git a/cmd/mesh-host/recovery_integration_test.go b/cmd/mesh-host/recovery_integration_test.go index 092e9090..2071af0d 100644 --- a/cmd/mesh-host/recovery_integration_test.go +++ b/cmd/mesh-host/recovery_integration_test.go @@ -5,6 +5,8 @@ package main import ( "context" "encoding/json" + "os" + "path/filepath" "testing" "time" @@ -78,6 +80,21 @@ func TestNatsTheRecoveryStepAsksHearsBoundsAndCatchesUpOverTheBus(t *testing.T) } t.Cleanup(func() { _ = toolSub.Unsubscribe() }) + // A-112: the bound is the number in the file the manifest declares, 90 where it is missing or not a number. + for content, want := range map[string]int{"45\n": 45, "": 90, "soon\n": 90, "0\n": 90} { + recoveryWaitFile = filepath.Join(t.TempDir(), "w") + if err := os.WriteFile(recoveryWaitFile, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + if got := recoveryWaitMinutes(); got != want { + t.Errorf("the file %q gives %d minutes, want %d", content, got, want) + } + } + recoveryWaitFile = filepath.Join(t.TempDir(), "missing") + if got := recoveryWaitMinutes(); got != 90 { + t.Errorf("no file gives %d minutes, want 90", got) + } + say("old", meshrecovery.RestoredEventOutcomeFailed) // said before anyone listens: the last message of the subject session := b.startSession() r := recoveryEnv(session, nil) diff --git a/module.json b/module.json index 9d3b87bd..74cdcb86 100644 --- a/module.json +++ b/module.json @@ -105,6 +105,13 @@ "path": "/var/lib/mesh-host/operator-keys", "mode": "0644", "content": "${config:operator-signers}\n" + }, + { + "id": "recovery-wait", + "type": "file", + "path": "/var/lib/mesh-host/recovery-wait-minutes", + "mode": "0644", + "content": "${config:recovery-wait-minutes}\n" } ] } diff --git a/packaging/operator_keys_test.go b/packaging/operator_keys_test.go index d4387d04..fdf38d77 100644 --- a/packaging/operator_keys_test.go +++ b/packaging/operator_keys_test.go @@ -76,6 +76,9 @@ func TestTheManifestConsumesTheBackupEvent(t *testing.T) { t.Errorf("invokes %v lacks %s", m.Invokes, v) } } + if !strings.Contains(string(raw), `"content": "${config:recovery-wait-minutes}\n"`) { + t.Errorf("module.json declares no file with the content ${config:recovery-wait-minutes} (design 47 G A-112)") + } if m.Config["recovery-wait-minutes"].Default != float64(90) { t.Errorf("config recovery-wait-minutes is %+v, want default 90", m.Config["recovery-wait-minutes"]) }