A-112: the node-engine reads recovery-wait-minutes from a file its manifest declares, 90 when missing

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
This commit is contained in:
2026-11-12 00:21:10 +01:00
parent dc1aee51d9
commit ece79788a3
4 changed files with 39 additions and 6 deletions
+12 -6
View File
@@ -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
@@ -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)
+7
View File
@@ -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"
}
]
}
+3
View File
@@ -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"])
}