diff --git a/tui/hookwise_tui/tabs/status.py b/tui/hookwise_tui/tabs/status.py index 0fa1617..8302f7d 100644 --- a/tui/hookwise_tui/tabs/status.py +++ b/tui/hookwise_tui/tabs/status.py @@ -490,23 +490,23 @@ def _render_segment(seg: str, cache: dict) -> str: except (ValueError, KeyError): pass suffix = f" (+{len(events) - 1} more)" if len(events) > 1 else "" - return f"\U0001f4c5 {current.get('title', '?')}{ends_in}{suffix}" - if not isinstance(next_event, dict) or not next_event.get("title"): + return f"\U0001f4c5 {current.get('name', '?')}{ends_in}{suffix}" + if not isinstance(next_event, dict) or not next_event.get("name"): return "\U0001f4c5 Free" try: start_ms = int(datetime.fromisoformat(next_event["start"].replace("Z", "+00:00")).timestamp() * 1000) except (ValueError, KeyError): - return f"\U0001f4c5 {next_event['title']}" + return f"\U0001f4c5 {next_event['name']}" now_ms = int(time.time() * 1000) diff_min = round((start_ms - now_ms) / 60000) other_count = len(events) - 1 if len(events) > 1 else 0 more_suffix = f" (+{other_count} more)" if other_count > 0 else "" if diff_min < 5: - text = f"\U0001f4c5 {next_event['title']} NOW" + text = f"\U0001f4c5 {next_event['name']} NOW" elif diff_min < 15: - text = f"\U0001f4c5 {next_event['title']} in {diff_min}min \u26a1" + text = f"\U0001f4c5 {next_event['name']} in {diff_min}min \u26a1" elif diff_min <= 60: - text = f"\U0001f4c5 {next_event['title']} in {diff_min}min" + text = f"\U0001f4c5 {next_event['name']} in {diff_min}min" else: hours = round(diff_min / 60) text = f"\U0001f4c5 Free for {hours}h" diff --git a/tui/tests/test_status_segments.py b/tui/tests/test_status_segments.py new file mode 100644 index 0000000..569a317 --- /dev/null +++ b/tui/tests/test_status_segments.py @@ -0,0 +1,63 @@ +"""Cross-boundary tests for StatusTab._render_segment(). + +These bind the Python status-line renderer to the EXACT field names the Go +feed producers emit (after FlattenForTUI spreads the envelope's data fields to +the top level of each cache entry). The renderer reads producer field names +directly -- there is no rename layer in data.py -- so a mismatch silently +degrades a segment in production (the Go->JSON->Python class, cf. weather bug +#29). See GitHub issue #155: calendar/project read names the producers never +emit. Canonical side: the Go producer (consistent + fixtured + tested); the +Python renderer adapts to it here. +""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +from hookwise_tui.tabs.status import StatusTab + + +class TestCalendarSegment: + """The calendar producer emits event 'name'; the renderer must read it.""" + + def test_current_event_renders_producer_name(self): + # Mirrors feeds.CalendarTestFixture(): events carry 'name', not 'title'. + cache = { + "calendar": { + "events": [ + { + "name": "Standup", + "start": "2026-03-07T10:30:00Z", + "end": "2999-01-01T00:00:00Z", # far future => "is current" + "all_day": False, + "is_current": True, + } + ], + "next_event": {"name": "Standup", "start": "2026-03-07T10:30:00Z"}, + } + } + out = StatusTab._render_segment("calendar", cache) + assert "Standup" in out, ( + "calendar must render the producer's event 'name'; reading 'title' " + "yields '?' (issue #155)" + ) + assert "?" not in out + + def test_next_event_renders_producer_name_not_free(self): + # No current event, one upcoming ~30min out (inside the <=60min naming + # window) -> must show its name. Reading 'title' here returned "Free" + # because next_event.get("title") was falsy (issue #155). + soon = (datetime.now(timezone.utc) + timedelta(minutes=30)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + cache = { + "calendar": { + "events": [{"name": "Standup", "start": soon, "is_current": False}], + "next_event": {"name": "Standup", "start": soon}, + } + } + out = StatusTab._render_segment("calendar", cache) + assert "Standup" in out, ( + "next_event must render the producer's 'name'; reading 'title' falls " + "through to 'Free' (issue #155)" + )