v0.263.2: the undo keeps the probe of the pinned version (R-637)
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
Found live on 9202 (romm): .felhom.yml flows into the stack dir on every catalog sync, so "the old .felhom.yml" saved at update time was already the new one, and the serving old version was judged with the new probe. New record applied-meta/.felhom.yml, written whenever a version is pinned (deploy, adoption, pin advance) and put back by the undo, like applied-compose.yml. The fixture now places the new file at sync time. 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:
@@ -4,6 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -117,14 +120,23 @@ const (
|
||||
undoVol = "nextcloud_db"
|
||||
)
|
||||
|
||||
// newUndoManager: slice 4's manager plus a data volume holding "OLD", the OLD .felhom.yml in the stack
|
||||
// dir, and a compose fake that plays the two things the real world does in between: the catalog's
|
||||
// .felhom.yml flows in with the new probe during the pull (§5.4 — .felhom.yml is never frozen), and
|
||||
// the NEW version migrates the data on its first `up`.
|
||||
// newUndoManager: slice 4's manager plus a data volume holding "OLD" and the world as the catalog
|
||||
// sync leaves it BEFORE anyone presses Update: the catalog's NEW .felhom.yml has already flowed into
|
||||
// the stack dir (§5.4 — .felhom.yml is never frozen; it arrives on the sync, not at the pull), while
|
||||
// the applied record (v0.263.2) still holds the pinned OLD version's file. The compose fake plays the
|
||||
// new version migrating the data on its first `up`.
|
||||
//
|
||||
// v0.263.1's version of this helper wrote the new .felhom.yml at PULL time — the wrong moment — and
|
||||
// so every undo test passed while the live box judged the old version with the new probe (9202,
|
||||
// 2026-09-23, romm). The order of events is part of the fixture.
|
||||
func newUndoManager(t *testing.T) (*Manager, string, *fakeGuards, *composeRec, *fakeCopier) {
|
||||
t.Helper()
|
||||
m, dir, g, c := newSlice4Manager(t)
|
||||
mustWrite(t, filepath.Join(dir, ".felhom.yml"), undoMetaOld)
|
||||
mustWrite(t, filepath.Join(dir, ".felhom.yml"), undoMetaNew)
|
||||
mustWrite(t, m.CatalogTemplatePath("nextcloud", ".felhom.yml"), undoMetaNew)
|
||||
if err := storeAppliedMeta(dir, []byte(undoMetaOld)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fc := newFakeCopier(map[string]string{undoVol: "OLD"})
|
||||
m.undoCopier = fc
|
||||
pulled, migrated := false, false
|
||||
@@ -132,7 +144,6 @@ func newUndoManager(t *testing.T) (*Manager, string, *fakeGuards, *composeRec, *
|
||||
switch args[0] {
|
||||
case "pull":
|
||||
pulled = true
|
||||
mustWrite(t, filepath.Join(dir, ".felhom.yml"), undoMetaNew)
|
||||
case "up":
|
||||
// Only the NEW version migrates: the first `up` after a pull, with the undo copy taken.
|
||||
// (After a failed copy, or in a resumed undo, the `up` starts the OLD version.)
|
||||
@@ -222,11 +233,14 @@ func TestUndo_CutOffCopyIsRefusedBeforeAnythingMoves(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestUndo_UsesTheOldProbe: the new .felhom.yml names a port the old version never answers (the drill
|
||||
// case, and a real one whenever a probe moves with a version). The undo must judge the old version
|
||||
// with the OLD probe.
|
||||
// case, and a real one whenever a probe moves with a version) — and it is ALREADY in the stack dir
|
||||
// when Update is pressed. The undo must judge the old version with the PINNED version's probe, taken
|
||||
// from the applied record.
|
||||
//
|
||||
// COMPANION RED-PROOF 3 (REPORT.md): make tryUndo use LoadMetadata(dir) (the current file) instead of
|
||||
// entry.PrevMeta — the undo then probes port 3999 and the app ends HELD; this test fails.
|
||||
// COMPANION RED-PROOF 3b: make savePreUpdateMeta read the stack dir's .felhom.yml instead of the
|
||||
// applied record (v0.263.1's shape) — the same failure, and the one the live box showed.
|
||||
func TestUndo_UsesTheOldProbe(t *testing.T) {
|
||||
m, _, g, _, _ := newUndoManager(t)
|
||||
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
||||
@@ -319,11 +333,11 @@ func TestUndo_NoRoomForTheCopyRefusesBeforeAnythingMoves(t *testing.T) {
|
||||
// falls to `default` (dropped), nothing is resumed, and this test fails at the first assertion.
|
||||
func TestUndo_PowerCutDuringTheUndoResumesIt(t *testing.T) {
|
||||
m, dir, g, _, fc := newUndoManager(t)
|
||||
e := simulateAdvanced(t, m, dir)
|
||||
md, err := savePreUpdateMeta(dir)
|
||||
md, err := savePreUpdateMeta(dir) // the job keeps the pinned version's file BEFORE the pin moves
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e := simulateAdvanced(t, m, dir)
|
||||
e.PrevMeta, e.Copied, e.Phase = md, true, UpdatePhaseUndoing
|
||||
e.NewPin = map[string]string{"web": "nextcloud:34.0.1-apache"}
|
||||
e.UndoCopies = []undoCopy{{Volume: undoVol, Copy: undoVol + ".pre-update-x"}}
|
||||
@@ -478,3 +492,81 @@ func TestUndo_OldProbeRunsOnAnAppTheCurrentProbeMarkedUnhealthy(t *testing.T) {
|
||||
t.Errorf("without an override an Unhealthy app must stay unhealthy and unprobed; ok=%v probed=%v", ok, probed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUndo_PinningRecordsThatVersionsProbe: every act that pins a version records its .felhom.yml,
|
||||
// and the undo's pin-back puts the previous record back.
|
||||
func TestUndo_PinningRecordsThatVersionsProbe(t *testing.T) {
|
||||
m, dir, _, _, _ := newUndoManager(t)
|
||||
appliedPort := func() int {
|
||||
meta := LoadMetadata(filepath.Join(dir, appliedMetaDir))
|
||||
if meta.HealthCheck == nil || len(meta.HealthCheck.Checks) == 0 {
|
||||
return 0
|
||||
}
|
||||
return meta.HealthCheck.Checks[0].Port
|
||||
}
|
||||
md, err := savePreUpdateMeta(dir)
|
||||
if err != nil || appliedPort() != 3000 {
|
||||
t.Fatalf("setup: md=%q err=%v applied=%d", md, err, appliedPort())
|
||||
}
|
||||
if err := m.advancePinToCatalog("nextcloud", dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := appliedPort(); got != 3999 {
|
||||
t.Errorf("advancing the pin must record the NEW version's .felhom.yml, applied port = %d", got)
|
||||
}
|
||||
m.restoreDefinition("nextcloud", dir, updateJournalEntry{PrevMeta: md, PrevPin: map[string]string{"web": "nextcloud:31.0.14-apache"}})
|
||||
if got := appliedPort(); got != 3000 {
|
||||
t.Errorf("the undo's pin-back must put the OLD record back, applied port = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUndo_NoAppliedRecordFallsBackLoudly: an app pinned before v0.263.2 has no applied record; the
|
||||
// copy is still made from the current file, and the caller is TOLD (the note) rather than left to
|
||||
// believe it holds the old probe.
|
||||
func TestUndo_NoAppliedRecordFallsBackLoudly(t *testing.T) {
|
||||
_, dir, _, _, _ := newUndoManager(t)
|
||||
if err := os.RemoveAll(filepath.Join(dir, appliedMetaDir)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
md, err := savePreUpdateMeta(dir)
|
||||
if md == "" || !errors.Is(err, errNoAppliedMeta) {
|
||||
t.Errorf("the fallback must still copy and must say so; md=%q err=%v", md, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUndo_EveryPinWriterRecordsTheProbe walks the AST: deploy, adoption and the pin advance each CALL
|
||||
// storeAppliedMetaFrom (a comment naming it does not count).
|
||||
func TestUndo_EveryPinWriterRecordsTheProbe(t *testing.T) {
|
||||
for file, fn := range map[string]string{"deploy.go": "runComposeDeploy", "pin.go": "AdoptPins"} {
|
||||
if !funcCalls(t, file, fn, "storeAppliedMetaFrom") {
|
||||
t.Errorf("%s.%s must call storeAppliedMetaFrom", file, fn)
|
||||
}
|
||||
}
|
||||
if !funcCalls(t, "pin.go", "advancePinToCatalog", "storeAppliedMetaFrom") {
|
||||
t.Error("advancePinToCatalog must call storeAppliedMetaFrom")
|
||||
}
|
||||
}
|
||||
|
||||
func funcCalls(t *testing.T, file, fn, callee string) bool {
|
||||
t.Helper()
|
||||
f, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range f.Decls {
|
||||
fd, ok := d.(*ast.FuncDecl)
|
||||
if !ok || fd.Name.Name != fn {
|
||||
continue
|
||||
}
|
||||
ast.Inspect(fd, func(n ast.Node) bool {
|
||||
if c, ok := n.(*ast.CallExpr); ok {
|
||||
if se, ok := c.Fun.(*ast.SelectorExpr); ok && se.Sel.Name == callee {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user