fix(tui): move all store writes out of Update into tea.Cmds
CI / test (pull_request) Successful in 2m35s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 51s

Deletes, pause toggles, maintenance end, theme/collapse prefs, and all
four form submits wrote to the store synchronously on the UI goroutine;
with busy_timeout=5000 a contended DB froze input for up to 5s.

Writes now run through a writeCmd helper returning writeDoneMsg. The
in-memory engine/model mutations stay in Update so rows react
instantly; the reply logs failures and reloads tab data, so the UI
converges on what was actually written. Closures capture snapshotted
values only — never the model.
This commit was merged in pull request #102.
This commit is contained in:
2026-06-11 11:39:15 -04:00
parent 634c3ee03c
commit a3711c652c
8 changed files with 168 additions and 74 deletions
+59 -6
View File
@@ -1,6 +1,7 @@
package tui
import (
"strings"
"testing"
"time"
@@ -19,6 +20,7 @@ type tuiMockStore struct {
maint []models.MaintenanceWindow
stateChanges []models.StateChange
stateChangeCalls int // counts GetStateChanges hits (to prove View does no IO)
deleteSiteCalls int // counts DeleteSite hits (to prove writes run in Cmds)
}
func (m *tuiMockStore) GetAllAlerts() ([]models.AlertConfig, error) { return m.alerts, nil }
@@ -32,12 +34,15 @@ func (m *tuiMockStore) GetAllMaintenanceWindows(int) ([]models.MaintenanceWindow
return m.maint, nil
}
func (m *tuiMockStore) Init() error { return nil }
func (m *tuiMockStore) GetSites() ([]models.Site, error) { return nil, nil }
func (m *tuiMockStore) AddSite(models.Site) error { return nil }
func (m *tuiMockStore) UpdateSite(models.Site) error { return nil }
func (m *tuiMockStore) UpdateSitePaused(int, bool) error { return nil }
func (m *tuiMockStore) DeleteSite(int) error { return nil }
func (m *tuiMockStore) Init() error { return nil }
func (m *tuiMockStore) GetSites() ([]models.Site, error) { return nil, nil }
func (m *tuiMockStore) AddSite(models.Site) error { return nil }
func (m *tuiMockStore) UpdateSite(models.Site) error { return nil }
func (m *tuiMockStore) UpdateSitePaused(int, bool) error { return nil }
func (m *tuiMockStore) DeleteSite(int) error {
m.deleteSiteCalls++
return nil
}
func (m *tuiMockStore) GetAlert(int) (models.AlertConfig, error) { return models.AlertConfig{}, nil }
func (m *tuiMockStore) AddAlert(string, string, map[string]string) error { return nil }
func (m *tuiMockStore) UpdateAlert(int, string, string, map[string]string) error { return nil }
@@ -309,6 +314,54 @@ func TestSLAData_DropsStaleReply(t *testing.T) {
}
}
func TestConfirmDelete_WritesOffUIGoroutine(t *testing.T) {
ms := &tuiMockStore{}
m := newTestModel(ms)
m.sites = []models.Site{{ID: 4, Name: "s"}}
m.state = stateConfirmDelete
m.deleteTab = 0
m.deleteID = 4
updated, cmd := (&m).handleConfirmDelete(keyMsg("y"))
if ms.deleteSiteCalls != 0 {
t.Fatal("delete hit the store synchronously in Update")
}
if cmd == nil {
t.Fatal("expected a write Cmd")
}
if got := updated.(*Model); got.state != stateDashboard {
t.Fatalf("expected return to dashboard, got state %v", got.state)
}
wd, ok := cmd().(writeDoneMsg)
if !ok || wd.err != nil {
t.Fatalf("unexpected write result: %+v", wd)
}
if ms.deleteSiteCalls != 1 {
t.Fatalf("expected exactly 1 store delete from the Cmd, got %d", ms.deleteSiteCalls)
}
}
func TestWriteDoneMsg_LogsErrorAndReloads(t *testing.T) {
m := newTestModel(&tuiMockStore{})
updated, cmd := m.Update(writeDoneMsg{op: "Delete site", err: errSentinel})
if cmd == nil {
t.Error("writeDoneMsg did not trigger a tab-data reload")
}
mm := updated.(Model)
found := false
for _, line := range mm.engine.GetLogs() {
if strings.Contains(line, "Delete site failed: boom") {
found = true
}
}
if !found {
t.Error("write error was not logged")
}
}
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
m := newTestModel(ms)