fix(tui): guard against late Progress/Log after DoneMsg; drop dead quitting field
This commit is contained in:
@@ -19,6 +19,8 @@ type DoneMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rowState tracks per-module progress in the dashboard.
|
// rowState tracks per-module progress in the dashboard.
|
||||||
|
// Name, Started, and Ended are populated for future use by elapsed-time
|
||||||
|
// rendering (planned for a later iteration); they are currently not read by View.
|
||||||
type rowState struct {
|
type rowState struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
@@ -41,7 +43,6 @@ type Model struct {
|
|||||||
showLogs bool
|
showLogs bool
|
||||||
summary *engine.Summary
|
summary *engine.Summary
|
||||||
runErr error
|
runErr error
|
||||||
quitting bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModel seeds the dashboard from the list of module IDs selected to run.
|
// NewModel seeds the dashboard from the list of module IDs selected to run.
|
||||||
@@ -65,7 +66,10 @@ func (m *Model) Init() tea.Cmd { return nil }
|
|||||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tui.ProgressMsg:
|
case tui.ProgressMsg:
|
||||||
if r, ok := m.rowIndex[msg.ModuleID]; ok {
|
// Guard: ignore late Progress events that arrive after DoneMsg has
|
||||||
|
// finalised this row. tea.Program.Send is asynchronous and concurrent
|
||||||
|
// goroutines have no arrival-order guarantee.
|
||||||
|
if r, ok := m.rowIndex[msg.ModuleID]; ok && !r.Done {
|
||||||
if r.Started.IsZero() {
|
if r.Started.IsZero() {
|
||||||
r.Started = time.Now()
|
r.Started = time.Now()
|
||||||
}
|
}
|
||||||
@@ -75,7 +79,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
r.StatusIcon = "⠋"
|
r.StatusIcon = "⠋"
|
||||||
}
|
}
|
||||||
case tui.LogMsg:
|
case tui.LogMsg:
|
||||||
if r, ok := m.rowIndex[msg.ModuleID]; ok {
|
if r, ok := m.rowIndex[msg.ModuleID]; ok && !r.Done {
|
||||||
r.LogTail = appendCapped(r.LogTail, msg.Line, 200)
|
r.LogTail = appendCapped(r.LogTail, msg.Line, 200)
|
||||||
}
|
}
|
||||||
case DoneMsg:
|
case DoneMsg:
|
||||||
@@ -101,7 +105,6 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q", "ctrl+c":
|
case "q", "ctrl+c":
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
|
|||||||
@@ -45,6 +45,28 @@ func TestDoneMsgFlipsIcons(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProgressAfterDoneDoesNotRevertIcon(t *testing.T) {
|
||||||
|
m := NewModel([]string{"a"}, nil)
|
||||||
|
m.Update(DoneMsg{Summary: engine.Summary{Results: []engine.ModuleResult{
|
||||||
|
{ID: "a", Status: engine.StatusSucceeded},
|
||||||
|
}}})
|
||||||
|
if got := m.rowIndex["a"].StatusIcon; got != "✓" {
|
||||||
|
t.Fatalf("after Done icon = %q, want ✓", got)
|
||||||
|
}
|
||||||
|
// Late Progress event — tea.Program.Send has no ordering guarantee across goroutines.
|
||||||
|
m.Update(tui.ProgressMsg{ModuleID: "a", Current: 9, Total: 10, Message: "late"})
|
||||||
|
if got := m.rowIndex["a"].StatusIcon; got != "✓" {
|
||||||
|
t.Fatalf("late Progress reverted icon to %q", got)
|
||||||
|
}
|
||||||
|
// Late Log event must also be dropped — it would otherwise pollute the
|
||||||
|
// log tail of a completed row.
|
||||||
|
before := len(m.rowIndex["a"].LogTail)
|
||||||
|
m.Update(tui.LogMsg{ModuleID: "a", Line: "late log"})
|
||||||
|
if after := len(m.rowIndex["a"].LogTail); after != before {
|
||||||
|
t.Fatalf("late Log appended to tail: before=%d after=%d", before, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestViewMentionsModules(t *testing.T) {
|
func TestViewMentionsModules(t *testing.T) {
|
||||||
m := NewModel([]string{"alpha", "bravo"}, nil)
|
m := NewModel([]string{"alpha", "bravo"}, nil)
|
||||||
out := m.View()
|
out := m.View()
|
||||||
|
|||||||
Reference in New Issue
Block a user