Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions tui/hookwise_tui/tabs/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
63 changes: 63 additions & 0 deletions tui/tests/test_status_segments.py
Original file line number Diff line number Diff line change
@@ -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)"
)
Loading