summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-25 22:58:40 +0200
committerPaul Buetow <paul@buetow.org>2026-02-25 22:58:40 +0200
commit4c34b9efcd539c819648c927d7e3f53220df8ad2 (patch)
treef9de9fd650a2d16316ba2c159990d891c9de5189 /internal/tui
parent67e10f34c92e93343adbd690b3b21e455e863bd3 (diff)
Fix stream paused scrolling and apply pending TUI/probe updates
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/dashboard/model.go13
-rw-r--r--internal/tui/dashboard/model_test.go85
-rw-r--r--internal/tui/eventstream/model.go88
-rw-r--r--internal/tui/eventstream/model_test.go137
-rw-r--r--internal/tui/eventstream/render.go2
-rw-r--r--internal/tui/eventstream/render_test.go24
-rw-r--r--internal/tui/probes/model.go4
-rw-r--r--internal/tui/probes/model_test.go41
-rw-r--r--internal/tui/tui.go21
-rw-r--r--internal/tui/tui_test.go37
10 files changed, 435 insertions, 17 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 8b2c814..9b425b1 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -73,6 +73,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
+ m.streamModel.SetViewport(msg.Width, msg.Height)
return m, nil
case refreshTickMsg:
snap := m.snapshot()
@@ -104,7 +105,10 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
prevActiveTab := m.activeTab
var cmd tea.Cmd
keyStr := msg.String()
- handled := m.handleArrowTabKey(keyStr) || m.handleScrollKey(keyStr)
+ handled := m.handleArrowTabKey(keyStr) || m.handleScrollKey(msg)
+ if handled && m.activeTab == TabStream && (keyStr == " " || keyStr == "space") && !m.streamModel.Paused() {
+ cmd = streamTickCmd()
+ }
if !handled {
switch {
@@ -171,7 +175,8 @@ func (m *Model) handleArrowTabKey(keyStr string) bool {
}
}
-func (m *Model) handleScrollKey(keyStr string) bool {
+func (m *Model) handleScrollKey(msg tea.KeyMsg) bool {
+ keyStr := msg.String()
switch m.activeTab {
case TabSyscalls:
return scrollOffset(keyStr, &m.syscallsOffset, m.maxSyscallsRows())
@@ -183,7 +188,9 @@ func (m *Model) handleScrollKey(keyStr string) bool {
case TabProcesses:
return scrollOffset(keyStr, &m.processesOffset, m.maxProcessesRows())
case TabStream:
- return m.streamModel.HandleKey(keyStr)
+ streamWidth, streamHeight := common.EffectiveViewport(m.width, m.height)
+ m.streamModel.SetViewport(streamWidth, streamHeight)
+ return m.streamModel.HandleTeaKey(msg)
default:
return false
}
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index c1b2e1d..1e54b27 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -1,11 +1,15 @@
package dashboard
import (
+ "fmt"
+ "regexp"
+ "strconv"
"strings"
"testing"
"ior/internal/statsengine"
common "ior/internal/tui/common"
+ "ior/internal/tui/eventstream"
"ior/internal/tui/messages"
tea "github.com/charmbracelet/bubbletea"
@@ -161,6 +165,87 @@ func TestFilesTabGroupedScrollUsesDirectoryOffset(t *testing.T) {
}
}
+func TestStreamSpaceUnpauseSchedulesStreamTick(t *testing.T) {
+ rb := eventstream.NewRingBuffer()
+ m := NewModelWithConfig(nil, rb, 250, common.DefaultKeyMap())
+ m.activeTab = TabStream
+ m.streamModel.HandleKey("space") // pause
+
+ next, cmd := m.Update(tea.KeyMsg{Type: tea.KeySpace})
+ _ = next
+ if cmd == nil {
+ t.Fatalf("expected stream tick command when unpausing stream")
+ }
+}
+
+func TestStreamPausedSupportsJKArrowsAndPageKeys(t *testing.T) {
+ rb := eventstream.NewRingBuffer()
+ for i := 0; i < 300; i++ {
+ rb.Push(eventstream.StreamEvent{
+ Seq: uint64(i + 1),
+ Syscall: "read",
+ Comm: "proc",
+ PID: 1000,
+ TID: uint32(2000 + i),
+ FileName: fmt.Sprintf("/tmp/file-%03d", i),
+ })
+ }
+
+ m := NewModelWithConfig(nil, rb, 250, common.DefaultKeyMap())
+ m.activeTab = TabStream
+ next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
+ m = next.(Model)
+
+ m.streamModel.Refresh()
+ _ = m.View()
+
+ next, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace}) // pause
+ m = next.(Model)
+ before := rowFromStreamView(t, m.View())
+
+ next, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}})
+ m = next.(Model)
+ afterK := rowFromStreamView(t, m.View())
+ if afterK >= before {
+ t.Fatalf("expected k to scroll up while paused: before=%d afterK=%d", before, afterK)
+ }
+
+ next, _ = m.Update(tea.KeyMsg{Type: tea.KeyDown})
+ m = next.(Model)
+ afterDown := rowFromStreamView(t, m.View())
+ if afterDown <= afterK {
+ t.Fatalf("expected down arrow to scroll down while paused: afterK=%d afterDown=%d", afterK, afterDown)
+ }
+
+ next, _ = m.Update(tea.KeyMsg{Type: tea.KeyPgUp})
+ m = next.(Model)
+ afterPgUp := rowFromStreamView(t, m.View())
+ if afterPgUp >= afterDown {
+ t.Fatalf("expected pgup to scroll up while paused: afterDown=%d afterPgUp=%d", afterDown, afterPgUp)
+ }
+
+ next, _ = m.Update(tea.KeyMsg{Type: tea.KeyPgDown})
+ m = next.(Model)
+ afterPgDown := rowFromStreamView(t, m.View())
+ if afterPgDown <= afterPgUp {
+ t.Fatalf("expected pgdown to scroll down while paused: afterPgUp=%d afterPgDown=%d", afterPgUp, afterPgDown)
+ }
+}
+
+func rowFromStreamView(t *testing.T, view string) int {
+ t.Helper()
+ re := regexp.MustCompile(`Row ([0-9]+)/([0-9]+)`)
+ m := re.FindStringSubmatch(view)
+ if len(m) != 3 {
+ t.Fatalf("stream row status not found in view")
+ }
+ row, err := strconv.Atoi(m[1])
+ if err != nil {
+ t.Fatalf("invalid row value %q: %v", m[1], err)
+ }
+ return row
+}
+
func TestDirGroupKeyTogglesOnlyOnFilesTab(t *testing.T) {
m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
m.activeTab = TabFiles
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go
index f51b7b5..0c50d0c 100644
--- a/internal/tui/eventstream/model.go
+++ b/internal/tui/eventstream/model.go
@@ -36,6 +36,17 @@ func NewModel(source *RingBuffer) Model {
}
}
+// SetViewport updates the render/scroll viewport dimensions used for
+// max-scroll and page-step calculations during key handling.
+func (m *Model) SetViewport(width, height int) {
+ if width > 0 {
+ m.width = width
+ }
+ if height > 0 {
+ m.height = height
+ }
+}
+
// SetSource updates the backing ring buffer and refreshes visible rows.
func (m *Model) SetSource(source *RingBuffer) {
m.source = source
@@ -47,6 +58,11 @@ func (m Model) FilterModalVisible() bool {
return m.filterModal.Visible()
}
+// Paused reports whether stream refresh is currently paused.
+func (m Model) Paused() bool {
+ return m.paused
+}
+
func (m *Model) HandleKey(keyStr string) bool {
if m.filterModal.Visible() {
wasVisible := m.filterModal.Visible()
@@ -66,10 +82,12 @@ func (m *Model) HandleKey(keyStr string) bool {
case " ", "space":
m.paused = !m.paused
if !m.paused {
+ // Resuming should return to live-tail behavior immediately.
+ m.autoScroll = true
m.Refresh()
}
return true
- case "f":
+ case "f", "F":
m.pauseBeforeFilter = m.paused
m.paused = true
m.filterModal = m.filterModal.Open(m.filter)
@@ -87,24 +105,44 @@ func (m *Model) HandleKey(keyStr string) bool {
m.applyFilter()
return true
case "j", "down":
- if m.scrollOffset < m.maxScrollOffset() {
- m.scrollOffset++
- }
- if m.scrollOffset < m.maxScrollOffset() {
- m.autoScroll = false
- }
+ m.scrollByLines(1)
return true
case "k", "up":
- if m.scrollOffset > 0 {
- m.scrollOffset--
- }
- m.autoScroll = false
+ m.scrollByLines(-1)
+ return true
+ case "pgdown", "pgdn", "pagedown":
+ m.scrollByLines(m.pageStep())
+ return true
+ case "pgup", "pageup":
+ m.scrollByLines(-m.pageStep())
return true
default:
return false
}
}
+// HandleTeaKey handles stream keys based on Bubble Tea key message types first,
+// then falls back to string matching for rune-driven shortcuts.
+func (m *Model) HandleTeaKey(msg tea.KeyMsg) bool {
+ switch msg.Type {
+ case tea.KeyUp:
+ return m.HandleKey("up")
+ case tea.KeyDown:
+ return m.HandleKey("down")
+ case tea.KeyPgUp:
+ return m.HandleKey("pgup")
+ case tea.KeyPgDown:
+ return m.HandleKey("pgdown")
+ case tea.KeySpace:
+ return m.HandleKey("space")
+ case tea.KeyRunes:
+ if len(msg.Runes) == 1 {
+ return m.HandleKey(string(msg.Runes[0]))
+ }
+ }
+ return m.HandleKey(msg.String())
+}
+
func (m *Model) View(width, height int) string {
if width <= 0 {
width = 100
@@ -198,6 +236,34 @@ func (m *Model) visibleRows() int {
return rows
}
+func (m *Model) pageStep() int {
+ rows := m.visibleRows()
+ if rows <= 1 {
+ return 1
+ }
+ return rows - 1
+}
+
+func (m *Model) scrollByLines(delta int) {
+ if delta == 0 {
+ return
+ }
+ max := m.maxScrollOffset()
+ next := m.scrollOffset + delta
+ if next < 0 {
+ next = 0
+ }
+ if next > max {
+ next = max
+ }
+ if next != m.scrollOffset {
+ m.scrollOffset = next
+ }
+ if m.scrollOffset < max {
+ m.autoScroll = false
+ }
+}
+
func keyMsgFromString(keyStr string) tea.KeyMsg {
switch keyStr {
case "esc":
diff --git a/internal/tui/eventstream/model_test.go b/internal/tui/eventstream/model_test.go
index 937bb33..bfcbca7 100644
--- a/internal/tui/eventstream/model_test.go
+++ b/internal/tui/eventstream/model_test.go
@@ -62,6 +62,76 @@ func TestModelScrollClamp(t *testing.T) {
}
}
+func TestModelPageScrollWithPgUpPgDown(t *testing.T) {
+ rb := NewRingBuffer()
+ m := NewModel(rb)
+ m.height = 12 // visibleRows=4, pageStep=3
+ pushEvents(rb, 30)
+ m.Refresh()
+ m.HandleKey("g")
+
+ if !m.HandleKey("pgdown") {
+ t.Fatalf("pgdown should be handled")
+ }
+ if m.scrollOffset != 3 {
+ t.Fatalf("expected page down to move by 3, got %d", m.scrollOffset)
+ }
+
+ if !m.HandleKey("pagedown") {
+ t.Fatalf("pagedown should be handled")
+ }
+ if m.scrollOffset != 6 {
+ t.Fatalf("expected pagedown alias to move by 3, got %d", m.scrollOffset)
+ }
+
+ if !m.HandleKey("pgup") {
+ t.Fatalf("pgup should be handled")
+ }
+ if m.scrollOffset != 3 {
+ t.Fatalf("expected page up to move up by 3, got %d", m.scrollOffset)
+ }
+ if !m.HandleKey("pageup") {
+ t.Fatalf("pageup should be handled")
+ }
+ if m.scrollOffset != 0 {
+ t.Fatalf("expected pageup alias to return to top, got %d", m.scrollOffset)
+ }
+}
+
+func TestModelArrowAndJKScroll(t *testing.T) {
+ rb := NewRingBuffer()
+ m := NewModel(rb)
+ m.height = 12
+ pushEvents(rb, 30)
+ m.Refresh()
+ m.HandleKey("g")
+
+ if !m.HandleKey("down") {
+ t.Fatalf("down should be handled")
+ }
+ if m.scrollOffset != 1 {
+ t.Fatalf("expected down to increment offset, got %d", m.scrollOffset)
+ }
+ if !m.HandleKey("j") {
+ t.Fatalf("j should be handled")
+ }
+ if m.scrollOffset != 2 {
+ t.Fatalf("expected j to increment offset, got %d", m.scrollOffset)
+ }
+ if !m.HandleKey("up") {
+ t.Fatalf("up should be handled")
+ }
+ if m.scrollOffset != 1 {
+ t.Fatalf("expected up to decrement offset, got %d", m.scrollOffset)
+ }
+ if !m.HandleKey("k") {
+ t.Fatalf("k should be handled")
+ }
+ if m.scrollOffset != 0 {
+ t.Fatalf("expected k to decrement offset, got %d", m.scrollOffset)
+ }
+}
+
func TestModelFilterReducesVisibleRows(t *testing.T) {
rb := NewRingBuffer()
m := NewModel(rb)
@@ -170,3 +240,70 @@ func TestFilterModalTemporarilyPausesAndRestoresState(t *testing.T) {
t.Fatalf("expected paused state preserved after modal close")
}
}
+
+func TestUnpauseRestoresLiveTailAndRefresh(t *testing.T) {
+ rb := NewRingBuffer()
+ m := NewModel(rb)
+ m.height = 10
+ pushEvents(rb, 20)
+ m.Refresh()
+
+ // Move off tail, then pause.
+ m.HandleKey("g")
+ if m.autoScroll {
+ t.Fatalf("expected autoScroll disabled at top")
+ }
+ m.HandleKey("space")
+ if !m.paused {
+ t.Fatalf("expected paused")
+ }
+
+ // New events arrive while paused.
+ pushEvents(rb, 5)
+ m.Refresh()
+
+ // Resume: should auto-tail and refresh immediately.
+ m.HandleKey("space")
+ if m.paused {
+ t.Fatalf("expected unpaused")
+ }
+ if !m.autoScroll {
+ t.Fatalf("expected autoScroll restored on resume")
+ }
+ if m.scrollOffset != m.maxScrollOffset() {
+ t.Fatalf("expected tail offset after resume, got offset=%d max=%d", m.scrollOffset, m.maxScrollOffset())
+ }
+}
+
+func TestPausedScrollWithJKAndPageKeys(t *testing.T) {
+ rb := NewRingBuffer()
+ m := NewModel(rb)
+ m.height = 20
+ pushEvents(rb, 100)
+ m.Refresh()
+ if !m.HandleKey("space") {
+ t.Fatalf("space should toggle pause")
+ }
+ before := rowNumber(m.scrollOffset, len(m.filtered))
+ if !m.HandleKey("k") {
+ t.Fatalf("k should be handled while paused")
+ }
+ afterK := rowNumber(m.scrollOffset, len(m.filtered))
+ if afterK >= before {
+ t.Fatalf("expected k to scroll up while paused: before=%d after=%d", before, afterK)
+ }
+ if !m.HandleKey("pgup") {
+ t.Fatalf("pgup should be handled while paused")
+ }
+ afterPgUp := rowNumber(m.scrollOffset, len(m.filtered))
+ if afterPgUp >= afterK {
+ t.Fatalf("expected pgup to scroll up while paused: afterK=%d afterPgUp=%d", afterK, afterPgUp)
+ }
+ if !m.HandleKey("pgdown") {
+ t.Fatalf("pgdown should be handled while paused")
+ }
+ afterPgDown := rowNumber(m.scrollOffset, len(m.filtered))
+ if afterPgDown <= afterPgUp {
+ t.Fatalf("expected pgdown to scroll down while paused: afterPgUp=%d afterPgDown=%d", afterPgUp, afterPgDown)
+ }
+}
diff --git a/internal/tui/eventstream/render.go b/internal/tui/eventstream/render.go
index e1781f8..24864b9 100644
--- a/internal/tui/eventstream/render.go
+++ b/internal/tui/eventstream/render.go
@@ -32,7 +32,7 @@ func RenderStreamTable(width int, paused bool, totalCount, filteredCount, buffer
lines = append(lines, renderEventRow(ev, contentWidth))
}
- return common.PanelStyle.Width(width).Render(strings.Join(lines, "\n"))
+ return common.PanelStyle.Width(contentWidth).Render(strings.Join(lines, "\n"))
}
func renderStatusLine(paused bool, totalCount, filteredCount, bufferLen, bufferCap int) string {
diff --git a/internal/tui/eventstream/render_test.go b/internal/tui/eventstream/render_test.go
index c7f32cd..65d7a61 100644
--- a/internal/tui/eventstream/render_test.go
+++ b/internal/tui/eventstream/render_test.go
@@ -3,6 +3,8 @@ package eventstream
import (
"strings"
"testing"
+
+ "github.com/charmbracelet/lipgloss"
)
func TestRenderStatusAndFilterLines(t *testing.T) {
@@ -100,3 +102,25 @@ func TestComputeColumnLayoutGivesFileMoreSpace(t *testing.T) {
t.Fatalf("expected file column to get most width, got %d", cols.file)
}
}
+
+func TestRenderStreamTableFitsRequestedWidth(t *testing.T) {
+ out := RenderStreamTable(80, false, 1, 1, 1, 10000, Filter{}, []StreamEvent{
+ {
+ Syscall: "read",
+ Comm: "worker",
+ PID: 1,
+ TID: 2,
+ DurationNs: 2000,
+ GapNs: 100,
+ Bytes: 64,
+ FileName: "/very/long/path/that/should/be/truncated/for/narrow/views/file.log",
+ RetVal: 1,
+ },
+ })
+
+ for _, line := range strings.Split(out, "\n") {
+ if lipgloss.Width(line) > 80 {
+ t.Fatalf("line exceeds width 80: %d %q", lipgloss.Width(line), line)
+ }
+ }
+}
diff --git a/internal/tui/probes/model.go b/internal/tui/probes/model.go
index dffa30f..5cec2c7 100644
--- a/internal/tui/probes/model.go
+++ b/internal/tui/probes/model.go
@@ -117,9 +117,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
return m, toggleCmd(m.manager, selected)
case "a":
- return m, bulkToggleCmd(m.manager, m.filtered(), false)
+ return m, bulkToggleCmd(m.manager, m.probes, false)
case "n":
- return m, bulkToggleCmd(m.manager, m.filtered(), true)
+ return m, bulkToggleCmd(m.manager, m.probes, true)
}
}
return m, nil
diff --git a/internal/tui/probes/model_test.go b/internal/tui/probes/model_test.go
index 74f6a6b..73a83bc 100644
--- a/internal/tui/probes/model_test.go
+++ b/internal/tui/probes/model_test.go
@@ -78,3 +78,44 @@ func TestToggleEmitsProbeToggledMsg(t *testing.T) {
}
_ = next
}
+
+func TestBulkKeysApplyGloballyNotOnlyFiltered(t *testing.T) {
+ fm := &fakeManager{
+ states: []probemanager.ProbeState{
+ {Syscall: "read", Active: true},
+ {Syscall: "write", Active: true},
+ {Syscall: "openat", Active: true},
+ },
+ }
+ m := NewModel(fm).Open()
+ m.search = "read"
+
+ _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}})
+ if cmd == nil {
+ t.Fatalf("expected bulk off command")
+ }
+ msg := cmd()
+ if toggled, ok := msg.(ProbeToggledMsg); !ok || toggled.Err != nil {
+ t.Fatalf("unexpected bulk off msg: %#v", msg)
+ }
+ if len(fm.toggles) != 3 {
+ t.Fatalf("expected all probes toggled off despite filter, got toggles=%+v", fm.toggles)
+ }
+
+ // Re-open with all inactive and filtered search still present; "a" should
+ // toggle all probes back on.
+ m = NewModel(fm).Open()
+ m.search = "read"
+ fm.toggles = nil
+ _, cmd = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}})
+ if cmd == nil {
+ t.Fatalf("expected bulk on command")
+ }
+ msg = cmd()
+ if toggled, ok := msg.(ProbeToggledMsg); !ok || toggled.Err != nil {
+ t.Fatalf("unexpected bulk on msg: %#v", msg)
+ }
+ if len(fm.toggles) != 3 {
+ t.Fatalf("expected all probes toggled on despite filter, got toggles=%+v", fm.toggles)
+ }
+}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 5dba75c..5a8c14b 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -12,6 +12,7 @@ import (
dashboardui "ior/internal/tui/dashboard"
"ior/internal/tui/eventstream"
tuiexport "ior/internal/tui/export"
+ "ior/internal/tui/messages"
"ior/internal/tui/pidpicker"
"ior/internal/tui/probes"
"os"
@@ -104,6 +105,21 @@ func getProbeManager() ProbeManager {
return probeManagerState.manager
}
+func resetDashboardSnapshotSource() *statsengine.Snapshot {
+ src := getDashboardSnapshotSource()
+ if src == nil {
+ return nil
+ }
+ if resettable, ok := src.(interface {
+ Reset()
+ Snapshot() *statsengine.Snapshot
+ }); ok {
+ resettable.Reset()
+ return resettable.Snapshot()
+ }
+ return nil
+}
+
// Run starts the TUI program in alternate screen mode.
func Run() error {
return RunWithTraceStarter(defaultTraceStarter)
@@ -233,6 +249,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case probes.ProbeToggledMsg:
var cmd tea.Cmd
m.probeModal, cmd = m.probeModal.Update(msg)
+ if snap := resetDashboardSnapshotSource(); snap != nil {
+ next, dashboardCmd := m.dashboard.Update(messages.StatsTickMsg{Snap: snap})
+ m.dashboard = next.(dashboardui.Model)
+ return m, tea.Batch(dashboardCmd, cmd)
+ }
return m, cmd
case PidSelectedMsg:
return m.handlePidSelected(msg)
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index 0f55b40..31a2e94 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -15,6 +15,7 @@ import (
"time"
"ior/internal/flags"
+ "ior/internal/tui/probes"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
@@ -186,6 +187,20 @@ func (f fakeDashboardSource) Snapshot() *statsengine.Snapshot {
return f.snap
}
+type fakeResettableDashboardSource struct {
+ snap *statsengine.Snapshot
+ resetCalls int
+}
+
+func (f *fakeResettableDashboardSource) Snapshot() *statsengine.Snapshot {
+ return f.snap
+}
+
+func (f *fakeResettableDashboardSource) Reset() {
+ f.resetCalls++
+ f.snap = &statsengine.Snapshot{TotalSyscalls: 0}
+}
+
func TestDashboardRefreshPicksLateBoundSource(t *testing.T) {
orig := getDashboardSnapshotSource()
defer SetDashboardSnapshotSource(orig)
@@ -202,6 +217,28 @@ func TestDashboardRefreshPicksLateBoundSource(t *testing.T) {
}
}
+func TestProbeToggledMsgResetsDashboardStatsSource(t *testing.T) {
+ src := &fakeResettableDashboardSource{snap: &statsengine.Snapshot{TotalSyscalls: 99}}
+ SetDashboardSnapshotSource(src)
+ t.Cleanup(func() { SetDashboardSnapshotSource(nil) })
+
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+ m.probeModal = probes.NewModel(fakeProbeManager{states: []probemanager.ProbeState{{Syscall: "read", Active: true}}}).Open()
+
+ next, _ := m.Update(probes.ProbeToggledMsg{Syscall: "read"})
+ updated := next.(Model)
+
+ if src.resetCalls != 1 {
+ t.Fatalf("expected one reset call, got %d", src.resetCalls)
+ }
+ snap := updated.dashboard.LatestSnapshot()
+ if snap == nil || snap.TotalSyscalls != 0 {
+ t.Fatalf("expected dashboard snapshot refreshed from reset source, got %+v", snap)
+ }
+}
+
func TestTracingStartedRebindsEventStreamSource(t *testing.T) {
orig := getEventStreamSource()
defer SetEventStreamSource(orig)