package stacks import ( "bytes" "context" "fmt" "log" "os" "path/filepath" "strings" "sync" "testing" "time" ) // R-717 — the command form's "open" twin: the household's 15-minute window reopens an app's own database-held // sign-up switch, and the loop closes it again. Never reaches Docker: afterLoadFn is the compose-exec seam. const cmdYml = "display_name: Gated App\nsetup_gate: true\nsignup_block: \"PathPrefix(`/signup`)\"\n" + "after_setup:\n service: gapp\n command: [\"sh\", \"-c\", \"close-signup\"]\n success: \"SIGNUP-CLOSED\"\n" + " open_command: [\"sh\", \"-c\", \"open-signup\"]\n open_success: \"SIGNUP-OPENED\"\n" + "deploy_fields:\n - env_var: DOMAIN\n type: domain\n - env_var: SUBDOMAIN\n type: subdomain\n default: gapp\n" // fakeSwitch is the app's own database switch behind the exec seam. type fakeSwitch struct { mu sync.Mutex open bool opens int closes int failOpen bool failClose bool onOpen func() // runs after the switch opened, before the command returns (a crash point) } func (f *fakeSwitch) exec(dir string, args ...string) (string, error) { f.mu.Lock() defer f.mu.Unlock() last := args[len(args)-1] switch last { case "open-signup": f.opens++ if f.failOpen { return "error: database is locked", fmt.Errorf("exit 1") } f.open = true if f.onOpen != nil { f.onOpen() } return "SIGNUP-OPENED", nil case "close-signup": f.closes++ if f.failClose { return "error: database is locked", fmt.Errorf("exit 1") } f.open = false return "SIGNUP-CLOSED", nil } return "", fmt.Errorf("unexpected exec %v", args) } func (f *fakeSwitch) counts() (opens, closes int, open bool) { f.mu.Lock() defer f.mu.Unlock() return f.opens, f.closes, f.open } func wireCmdManager(m *Manager, f *fakeSwitch) *bytes.Buffer { m.afterSetupSync = true m.afterSetupUpFn = func(string) error { return nil } m.afterLoadFn = f.exec var buf bytes.Buffer m.logger = log.New(&buf, "", 0) return &buf } func cmdManager(t *testing.T, yml string) (*Manager, *fakeSwitch, *bytes.Buffer, string) { t.Helper() m := gateManager(t, yml) f := &fakeSwitch{} buf := wireCmdManager(m, f) dir := closedGate(t, m) must(t, m.OpenSetupGate("gapp", SetupGateByHousehold)) // the first close if _, closes, open := f.counts(); closes != 1 || open { t.Fatalf("the gate opening did not close the app's own switch: closes=%d open=%v", closes, open) } return m, f, buf, dir } // The window runs open_command once; the window's end runs command once; the switch ends closed. // COMPANION RED-PROOF (felhom.eu audits/design-build-2026-10-06/E/red-open-then-close.txt): drop the open_command // run in liftNativeLock (the code before R-717) → "the window did not open the app's own switch" fails. func TestAfterSetupR717_OpenThenCloseEachRunOnce(t *testing.T) { m, f, _, dir := cmdManager(t, cmdYml) _, err := m.OpenSignupWindow("gapp") must(t, err) if opens, closes, open := f.counts(); opens != 1 || closes != 1 || !open { t.Fatalf("the window did not open the app's own switch: opens=%d closes=%d open=%v", opens, closes, open) } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockLifted || c.AfterSetup == nil || !c.AfterSetup.OK { t.Fatalf("after the open: native=%q record=%+v", c.SetupGate.NativeLock, c.AfterSetup) } m.SetupGateTick() // inside the window: nothing closes if _, closes, open := f.counts(); closes != 1 || !open { t.Fatalf("the loop closed the switch INSIDE the window: closes=%d open=%v", closes, open) } later := time.Now().Add(signupWindow + time.Minute) m.updateNowFn = func() time.Time { return later } m.SetupGateTick() m.SetupGateTick() // a second pass must not run it again if opens, closes, open := f.counts(); opens != 1 || closes != 2 || open { t.Fatalf("after the window: opens=%d closes=%d open=%v (want 1 open, 1 more close, closed)", opens, closes, open) } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockApplied || !c.AfterSetup.OK { t.Fatalf("after the window: native=%q record=%+v", c.SetupGate.NativeLock, c.AfterSetup) } } // A box restart inside the window ends CLOSED — even when the controller died between the open command and its // record (the worst moment: the switch is open, nothing says so yet). // COMPANION RED-PROOF (red-restart-mid-open.txt): drop the "opening" mark in liftNativeLock → the disk at the crash // says "applied", the restarted loop never closes, "a restart left sign-up OPEN" fails. func TestAfterSetupR717_ARestartInsideTheWindowEndsClosed(t *testing.T) { m, f, _, dir := cmdManager(t, cmdYml) var atCrash []byte f.onOpen = func() { // the controller dies here: copy app.yaml as it is on disk at this moment b, err := os.ReadFile(filepath.Join(dir, "app.yaml")) must(t, err) atCrash = b } _, err := m.OpenSignupWindow("gapp") must(t, err) if atCrash == nil { t.Fatal("the open command never ran") } must(t, os.WriteFile(filepath.Join(dir, "app.yaml"), atCrash, 0o600)) // the disk the restart finds // The restart: a new Manager over the same paths, the startup tick (RunSetupGateLoop's first pass). m2, err := NewManager(m.cfg, log.New(&bytes.Buffer{}, "", 0)) must(t, err) must(t, m2.ScanStacks()) buf := wireCmdManager(m2, f) m2.mu.Lock() m2.stacks["gapp"].State = StateRunning m2.mu.Unlock() later := time.Now().Add(signupWindow + time.Minute) m2.updateNowFn = func() time.Time { return later } m2.SetupGateTick() if _, _, open := f.counts(); open { t.Fatalf("a restart left sign-up OPEN in the app past its window (record at the crash: native=%q)\n%s", LoadAppConfig(dir).SetupGate.NativeLock, buf.String()) } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockApplied { t.Fatalf("after the restart: native=%q", c.SetupGate.NativeLock) } } // An open command that fails leaves the switch closed, records step "open", and says so in the log. // COMPANION RED-PROOF (red-failed-open.txt): drop the re-close (applyNativeLock(true, …)) in liftNativeLock's // failure branch → the half-opened switch stays open → "a failed open left the switch OPEN" fails. func TestAfterSetupR717_AFailedOpenStaysClosedAndSaysSo(t *testing.T) { m, f, buf, dir := cmdManager(t, cmdYml) f.onOpen = nil f.mu.Lock() f.failOpen = true f.open = true // the command half-ran: it flipped the switch, then failed f.mu.Unlock() _, err := m.OpenSignupWindow("gapp") must(t, err) if _, _, open := f.counts(); open { t.Fatalf("a failed open left the switch OPEN\n%s", buf.String()) } c := LoadAppConfig(dir) if c.SetupGate.NativeLock == NativeLockLifted || c.SetupGate.NativeLock == NativeLockOpening { t.Fatalf("a failed open recorded native=%q", c.SetupGate.NativeLock) } if c.AfterSetup == nil || c.AfterSetup.OK || c.AfterSetup.Step != afterSetupStepOpen || !strings.Contains(c.AfterSetup.Detail, "could not be opened") { t.Fatalf("the failure is not recorded: %+v", c.AfterSetup) } if !strings.Contains(buf.String(), "[ERROR]") || !strings.Contains(buf.String(), "could NOT be opened") { t.Fatalf("the failure is not logged:\n%s", buf.String()) } } // A close that fails at the window's end is logged loudly, never recorded as closed, and retried by the loop. // COMPANION RED-PROOF (red-failed-close-retry.txt): use the 30-minute nativeLockRetry after a window too → "the loop // did not retry the close" fails. func TestAfterSetupR717_AFailedCloseIsRetriedNotTrusted(t *testing.T) { m, f, buf, dir := cmdManager(t, cmdYml) _, err := m.OpenSignupWindow("gapp") must(t, err) f.mu.Lock() f.failClose = true f.mu.Unlock() t0 := time.Now().Add(signupWindow + time.Minute) m.updateNowFn = func() time.Time { return t0 } m.SetupGateTick() c := LoadAppConfig(dir) if c.SetupGate.NativeLock == NativeLockApplied || c.AfterSetup.OK { t.Fatalf("a failed close was recorded as closed: native=%q record=%+v", c.SetupGate.NativeLock, c.AfterSetup) } if !strings.Contains(buf.String(), "may still be OPEN past the household's window") { t.Fatalf("the failed close is not loud:\n%s", buf.String()) } f.mu.Lock() f.failClose = false f.mu.Unlock() t1 := t0.Add(nativeLockOpenRetry + time.Second) m.updateNowFn = func() time.Time { return t1 } m.SetupGateTick() if _, _, open := f.counts(); open { t.Fatal("the loop did not retry the close") } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockApplied || !c.AfterSetup.OK { t.Fatalf("after the retry: native=%q record=%+v", c.SetupGate.NativeLock, c.AfterSetup) } } // After a successful app update, the loop runs the close again. // COMPANION RED-PROOF (red-close-after-update.txt): drop markNativeLockForReapply in verifyAndConclude → the close // does not run again → "the update did not re-run the close" fails. func TestAfterSetupR717_TheCloseRunsAgainAfterAnUpdate(t *testing.T) { m, f, _, dir := cmdManager(t, cmdYml) m.updateHealthFn = func(context.Context, string, time.Duration) (bool, string) { return true, "ok" } f.mu.Lock() f.open = true // the update brought a database whose switch is open f.mu.Unlock() m.verifyAndConclude(context.Background(), "gapp", dir, nil, UpdateRestorePoint{}, time.Now(), &updateJournalEntry{}) m.mu.Lock() m.stacks["gapp"].State = StateRunning m.mu.Unlock() m.SetupGateTick() if _, closes, open := f.counts(); closes != 2 || open { t.Fatalf("the update did not re-run the close: closes=%d open=%v", closes, open) } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockApplied { t.Fatalf("after the update: native=%q", c.SetupGate.NativeLock) } } // A template that closes by command but has no open_command: the window opens the address only, the app's switch // stays closed, and the log says so ONCE (two presses). // COMPANION RED-PROOF (red-no-open-command-logs-once.txt): drop the LoadOrStore guard → logged twice, fails. func TestAfterSetupR717_NoOpenCommandLogsOnceAndKeepsTheWindow(t *testing.T) { yml := strings.Replace(cmdYml, " open_command: [\"sh\", \"-c\", \"open-signup\"]\n open_success: \"SIGNUP-OPENED\"\n", "", 1) m, f, buf, dir := cmdManager(t, yml) for i := 0; i < 2; i++ { _, err := m.OpenSignupWindow("gapp") must(t, err) } if opens, closes, open := f.counts(); opens != 0 || closes != 1 || open { t.Fatalf("opens=%d closes=%d open=%v", opens, closes, open) } if n := strings.Count(buf.String(), "has no open_command"); n != 1 { t.Fatalf("the missing open_command was logged %d times, want once:\n%s", n, buf.String()) } if _, err := os.Stat(m.signupBlockPath("gapp")); !os.IsNotExist(err) { t.Fatal("the window did not open the address") } if c := LoadAppConfig(dir); c.SetupGate.NativeLock != NativeLockApplied { t.Fatalf("native=%q — nothing of the app's own was opened", c.SetupGate.NativeLock) } } // The env form keeps today's behaviour beside an open_command: env lifted AND the command run. func TestAfterSetupR717_EnvAndOpenCommandTogether(t *testing.T) { yml := strings.Replace(cmdYml, "after_setup:\n", "after_setup:\n env:\n SIGNUP_CLOSED: \"true\"\n", 1) m := gateManager(t, yml) must(t, os.WriteFile(filepath.Join(m.cfg.Paths.StacksDir, "gapp", "docker-compose.yml"), []byte(nativeCompose), 0o644)) must(t, m.ScanStacks()) f := &fakeSwitch{} wireCmdManager(m, f) dir := closedGate(t, m) must(t, m.OpenSetupGate("gapp", SetupGateByHousehold)) _, err := m.OpenSignupWindow("gapp") must(t, err) c := LoadAppConfig(dir) if opens, _, open := f.counts(); c.Env["SIGNUP_CLOSED"] != "" || opens != 1 || !open || c.SetupGate.NativeLock != NativeLockLifted { t.Fatalf("window: env=%q opens=%d open=%v native=%q", c.Env["SIGNUP_CLOSED"], opens, open, c.SetupGate.NativeLock) } later := time.Now().Add(signupWindow + time.Minute) m.updateNowFn = func() time.Time { return later } m.SetupGateTick() c = LoadAppConfig(dir) if _, _, open := f.counts(); c.Env["SIGNUP_CLOSED"] != "true" || open || c.SetupGate.NativeLock != NativeLockApplied { t.Fatalf("after: env=%q open=%v native=%q", c.Env["SIGNUP_CLOSED"], open, c.SetupGate.NativeLock) } }