v0.270.0: no update for a current app (R-679); an interrupted install is reported (R-681); a restore brings back the pinned version's health check (R-669); R-674
gates / gates (push) Successful in 24s

Five red-proofs. MinAgent 0.131.0 unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-24 16:49:43 +02:00
parent 7c6140d95b
commit 504eae018b
23 changed files with 1121 additions and 16 deletions
@@ -0,0 +1,57 @@
package backup
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// R-669 (v0.270.0): the recovery unit captures the PINNED version's .felhom.yml (the applied record),
// not the stack dir's — which the sync may already have replaced with a newer, even failing, step's file.
// MEASURED 2026-09-24 (A2): the unit held port 8999 (the failing step's probe), a restore wrote it back,
// and the next undo judged the correct version with it and held the app.
//
// COMPANION RED-PROOF (REPORT.md): capture the stack dir's .felhom.yml again → "the unit carries the
// failing step's probe".
func TestR669_UnitCapturesThePinnedVersionsMeta(t *testing.T) {
tmp := t.TempDir()
stackDir := filepath.Join(tmp, "stack")
drive := filepath.Join(tmp, "drive")
mustWrite(t, filepath.Join(stackDir, "docker-compose.yml"), "services:\n app:\n image: example/app:1.2.3\n")
mustWrite(t, filepath.Join(stackDir, ".felhom.yml"), "display_name: Example\nhealthcheck:\n checks:\n - type: api\n port: 8999\n") // flowed by the sync
mustWrite(t, stacks.AppliedMetaFile(stackDir), "display_name: Example\nhealthcheck:\n checks:\n - type: api\n port: 80\n") // the pinned version's
mustWrite(t, filepath.Join(stackDir, "app.yaml"), "deployed: true\nenv: {}\n")
m := &Manager{
logger: log.New(io.Discard, "", 0),
systemDataPath: filepath.Join(tmp, "system"),
stackProvider: &fakeRecoveryProvider{info: RecoveryInfo{StackDir: stackDir, DisplayName: "Example", ImagePins: []string{"example/app:1.2.3"}}, hdd: drive},
version: "vtest",
}
if err := m.CaptureRecoveryUnit("example"); err != nil {
t.Fatalf("capture: %v", err)
}
b, err := os.ReadFile(filepath.Join(RecoveryUnitComposePath(drive, "example"), ".felhom.yml"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(b), "8999") || !strings.Contains(string(b), "port: 80") {
t.Fatalf("the unit carries the failing step's probe, not the pinned version's:\n%s", b)
}
// Control: no applied record (an app pinned before v0.263.2) → the stack dir's file, as before.
if err := os.RemoveAll(filepath.Dir(stacks.AppliedMetaFile(stackDir))); err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(stackDir, ".felhom.yml"), "display_name: Example changed\n") // force a recapture
if err := m.CaptureRecoveryUnit("example"); err != nil {
t.Fatal(err)
}
b, _ = os.ReadFile(filepath.Join(RecoveryUnitComposePath(drive, "example"), ".felhom.yml"))
if !strings.Contains(string(b), "Example changed") {
t.Fatalf("without an applied record the stack dir's file must be captured: %s", b)
}
}
+12 -1
View File
@@ -13,6 +13,7 @@ import (
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
"gopkg.in/yaml.v3"
)
@@ -115,7 +116,17 @@ func (m *Manager) CaptureRecoveryUnit(stackName string) error {
checksums := make(map[string]string)
var configFiles []string
for _, fname := range []string{"docker-compose.yml", ".felhom.yml"} {
data, err := os.ReadFile(filepath.Join(info.StackDir, fname))
src := filepath.Join(info.StackDir, fname)
// R-669 (v0.270.0): the .felhom.yml that belongs to the PINNED version — the one the compose above
// runs — is the applied record. The stack dir's own file flows from the catalog on every sync and
// can already be a NEWER (even a failing) version's; restored with the unit, it became the pinned
// version's probe and an undo judged the correct version unhealthy (measured 2026-09-24, A2).
if fname == ".felhom.yml" {
if _, err := os.Stat(stacks.AppliedMetaFile(info.StackDir)); err == nil {
src = stacks.AppliedMetaFile(info.StackDir)
}
}
data, err := os.ReadFile(src)
if err != nil {
continue // optional — capture whichever exist
}