Files
admin 2f8ff2414c
gates / gates (push) Successful in 17s
v0.244.0: the backup page stops promising what it does not hold (R-537/R-538/R-536)
R-537 — the contents label is now PER TIER. One string computed from the app's
shape was rendered on all three tier rows; a Tier-1 unit has no file-copy step, so
for the four class-A apps it was claiming „Adatok" for files it does not hold.

R-538 — a unit restore REFUSES before anything is touched when the unit cannot
return the app's drive-side files, and names the route that can. It runs before the
stack is stopped because the measured harm included the app's own wastebasket going
unreachable, which still held every byte.

R-536 — „Alkalmazás telepítve" moved from the deploy's acceptance to its completion,
with app_deploy_started and app_deploy_failed as the honest pair.

Each fix red-proofed: seen failing with its own sentence, passing when restored.
Requires hub v0.116.0 for the two new event types. MinAgent unchanged (0.131.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-16 16:55:55 +02:00

82 lines
3.1 KiB
Go

package stacks
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// withFakeComposeExit is withFakeCompose with a chosen exit code, so the FAILING deploy can be
// driven through the same process boundary as the succeeding one.
func withFakeComposeExit(t *testing.T, m *Manager, code int) {
t.Helper()
if runtime.GOOS != "linux" {
t.Skip("the stub compose binary is a shell script")
}
bin := t.TempDir()
script := "#!/bin/sh\nexit " + string(rune('0'+code)) + "\n"
if err := os.WriteFile(filepath.Join(bin, "docker-compose"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
m.composeCmd = "docker-compose"
m.execFn = func(string, ...string) (string, error) { return "", nil }
}
// R-536 — „Alkalmazás telepítve" must be said at the END of a deploy, and a deploy that ends badly
// must say THAT rather than nothing.
//
// The defect this pins, measured live on 2026-09-16: the deploy of `mealie` was accepted at
// 12:31:36 CEST and the hub logged „Alkalmazás telepítve: Mealie" in the same second; the controller
// was killed five seconds later, and after the agent restarted it the stack read
// `not_deployed / deployed=false / deploying=false`. Nothing ever corrected the event.
//
// Red-proof: remove the deployDoneHook call from the success path → the "installed" case fails;
// remove it from the failure branch → the "failed" case fails.
func TestDeployDoneHook_FiresAtTheEndAndSaysWhichEndItWas(t *testing.T) {
t.Run("a deploy that succeeds reports installed", func(t *testing.T) {
m, dir := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n")
withFakeCompose(t, m)
var gotName, gotDetail string
var gotOK, fired bool
m.SetDeployDoneHook(func(name string, ok bool, detail string) {
fired, gotName, gotOK, gotDetail = true, name, ok, detail
})
m.runComposeDeploy("bookstack", dir, map[string]string{}, &AppConfig{Deployed: true})
if !fired {
t.Fatal("the deploy ended and nothing was told about it")
}
if gotName != "bookstack" || !gotOK {
t.Fatalf("a successful deploy must report ok for its own app: name=%q ok=%v detail=%q", gotName, gotOK, gotDetail)
}
})
t.Run("a deploy that fails says so", func(t *testing.T) {
m, dir := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n")
withFakeComposeExit(t, m, 1)
var gotOK, fired bool
var gotDetail string
m.SetDeployDoneHook(func(_ string, ok bool, detail string) {
fired, gotOK, gotDetail = true, ok, detail
})
m.runComposeDeploy("bookstack", dir, map[string]string{}, &AppConfig{Deployed: true})
if !fired {
t.Fatal("a failed deploy must be reported, not be silence — silence is what left a completed-install record for an app that never ran")
}
if gotOK {
t.Fatalf("a failed deploy must not report ok (detail=%q)", gotDetail)
}
// And the durable record must read not-deployed, which is the fact every surface reads.
if cfg := LoadAppConfig(dir); cfg != nil && cfg.Deployed {
t.Fatal("a failed deploy must leave the durable record NOT deployed")
}
})
}