fix: follow live activity until inspection (#102)

This commit is contained in:
xavierk
2026-09-28 16:32:13 +05:30
parent 3e1ecc90e4
commit f780212a02
4 changed files with 442 additions and 60 deletions
+94
View File
@@ -0,0 +1,94 @@
"""Activity-selection transitions independent from Textual widgets."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.activity_selection import ActivitySelection, IntervalIdentity
def _interval(n):
return IntervalIdentity(f"start-{n}", f"end-{n}")
def test_live_selection_starts_and_stays_on_newest_interval():
selection = ActivitySelection()
first = [_interval(1), _interval(2)]
selection.update_live(first)
assert selection.following_live
assert selection.selected_live_interval == first[-1]
second = first + [_interval(3)]
selection.update_live(second)
assert selection.selected_live_interval == second[-1]
def test_keyboard_and_mouse_inspection_pin_interval_identity():
intervals = [_interval(1), _interval(2), _interval(3)]
keyboard = ActivitySelection()
mouse = ActivitySelection()
keyboard.update_live(intervals)
keyboard.move_live(intervals, -1)
mouse.update_live(intervals)
mouse.inspect_live(intervals, intervals[1])
assert keyboard.selected_live_interval == mouse.selected_live_interval == intervals[1]
assert not keyboard.following_live
assert not mouse.following_live
refreshed = [_interval(2), _interval(3), _interval(4)]
keyboard.update_live(refreshed)
mouse.update_live(refreshed)
assert keyboard.selected_live_interval == mouse.selected_live_interval == intervals[1]
def test_expired_pin_explains_expiration_and_resumes_following():
selection = ActivitySelection()
intervals = [_interval(1), _interval(2), _interval(3)]
selection.update_live(intervals)
selection.inspect_live(intervals, intervals[0])
newest = _interval(4)
selection.update_live([_interval(2), _interval(3), newest])
assert selection.following_live
assert selection.selected_live_interval == newest
assert selection.live_interval_expired
def test_today_resets_expired_pin_and_read_write_toggle_keeps_identity():
selection = ActivitySelection()
intervals = [_interval(1), _interval(2)]
selection.update_live(intervals)
selection.inspect_live(intervals, intervals[0])
selection.update_live([intervals[1]])
newest = _interval(3)
selection.update_live([intervals[1], newest])
assert selection.live_interval_expired
selection.toggle_measure()
assert selection.measure == "read"
assert selection.selected_live_interval == newest
selection.select_history_date("2026-09-27")
selection.set_view("history")
selection.set_view("live")
assert selection.browse_date is None
assert selection.following_live
assert not selection.live_interval_expired
def test_live_identity_uses_both_interval_boundaries():
first = IntervalIdentity("2026-09-28T10:00:00Z", "2026-09-28T10:03:00Z")
changed_start = IntervalIdentity("2026-09-28T09:57:00Z", "2026-09-28T10:03:00Z")
selection = ActivitySelection()
selection.update_live([first])
selection.inspect_live([first], first)
selection.update_live([changed_start])
assert selection.following_live
assert selection.selected_live_interval == changed_start
assert selection.live_interval_expired
+108 -7
View File
@@ -6,26 +6,24 @@ Covers:
- Three-minute cadence constants
- TUI integration with live graph
"""
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store
from fenris.activity_selection import IntervalIdentity
from fenris.monitoring_periods import ensure_period_open
from fenris.status import ACCURACY_SEC, CADENCE_DEFAULT_S, FRESH_THRESHOLD_S
from fenris.store import init_store
from fenris.tui import (
LIVE_WINDOW_H,
FenrisTuiApp,
LiveActivityGraph,
_query_live_graph_data,
LIVE_WINDOW_H,
_query_daily_graph_data,
_RANGE_OPTIONS,
)
from fenris.status import CADENCE_DEFAULT_S, FRESH_THRESHOLD_S, ACCURACY_SEC
# ---------------------------------------------------------------------------
# Helpers
@@ -63,6 +61,27 @@ def _open_period(conn, start="2026-09-01T00:00:00+00:00"):
ensure_period_open(conn, datetime.fromisoformat(start))
def _live_intervals(count=4):
first = datetime(2026, 9, 28, 12, 0, tzinfo=timezone.utc)
intervals = []
for index in range(count):
start = first + timedelta(minutes=3 * index)
end = start + timedelta(minutes=3)
intervals.append({
"start_ts": start.isoformat(),
"end_ts": end.isoformat(),
"start_label": start.strftime("%H:%M"),
"end_label": end.strftime("%H:%M"),
"bytes_written": (index + 1) * 1000,
"bytes_read": (index + 1) * 500,
"elapsed_s": 180,
"is_gap": False,
"is_zero": False,
"is_segment_boundary": False,
})
return intervals
# ---------------------------------------------------------------------------
# Cadence constants (issue #91 AC1)
# ---------------------------------------------------------------------------
@@ -322,3 +341,85 @@ class TestTUILiveIntegration:
await pilot.pause()
main_grid = app.query_one("#main-grid")
assert main_grid.has_class("constrained")
@pytest.mark.asyncio
@pytest.mark.parametrize("size", [(100, 40), (70, 20)])
async def test_follow_pin_expiry_and_today_use_visible_readout(self, tmp_path, size):
"""Live selection follows, pins, expires, and resets at both layout sizes."""
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
intervals = _live_intervals()
async with app.run_test(size=size) as pilot:
await pilot.pause()
graph = app.query_one("#live-activity")
graph.set_data(intervals[:3])
readout = str(app.query_one("#live-readout").render())
assert "12:09 UTC" in readout
assert graph.selection.following_live
assert graph.selection.selected_live_interval == IntervalIdentity(
intervals[2]["start_ts"], intervals[2]["end_ts"]
)
# Refresh while following selects the newly measured newest point.
graph.set_data(intervals)
assert "12:12 UTC" in str(app.query_one("#live-readout").render())
# Live graph receives focus at launch; first arrow pins immediately.
await pilot.press("left")
pinned = graph.selection.selected_live_interval
readout = str(app.query_one("#live-readout").render())
assert "12:09 UTC" in readout
assert not graph.selection.following_live
graph.set_data(intervals)
readout = str(app.query_one("#live-readout").render())
assert "12:09 UTC" in readout
assert graph.selection.selected_live_interval == pinned
# Read/write changes presentation only; pinned point stays stable.
await pilot.press("w")
graph.set_data(intervals)
assert graph.measure == "read"
assert graph.selection.selected_live_interval == pinned
assert "12:09 UTC" in str(app.query_one("#live-readout").render())
# Remove pinned interval from rolling window. Explain expiration.
graph.set_data(intervals[3:])
readout = str(app.query_one("#live-readout").render())
assert "Inspected interval expired; following live" in readout
assert "12:12 UTC" in readout
assert graph.selection.following_live
# Today action clears expiration and resumes newest-point following.
await pilot.press("t")
assert graph.selection.following_live
assert not graph.selection.live_interval_expired
graph.set_data(intervals)
readout = str(app.query_one("#live-readout").render())
assert "12:12 UTC" in readout
assert "Inspected interval expired" not in readout
assert graph.selection.following_live
@pytest.mark.asyncio
async def test_mouse_inspection_pins_same_interval_identity(self, tmp_path):
"""Mouse inspection pins the interval at its visible graph location."""
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
intervals = _live_intervals()
async with app.run_test(size=(100, 40)) as pilot:
await pilot.pause()
graph = app.query_one("#live-activity")
graph.set_data(intervals[:3])
await pilot.pause()
render = app.query_one("#live-render")
await pilot.click(
"#live-render",
offset=(8, max(0, render.content_size.height // 2)),
)
await pilot.pause()
expected = IntervalIdentity(
intervals[0]["start_ts"], intervals[0]["end_ts"]
)
assert graph.selection.selected_live_interval == expected
assert not graph.selection.following_live
assert "12:03 UTC" in str(app.query_one("#live-readout").render())