Skip to content

Commit e21f208

Browse files
vishnujayvelclaude
andcommitted
fix(tui): calendar segment reads producer event 'name' not 'title'
The calendar status-line renderer read event 'title', but the Go calendar producer (and CalendarTestFixture) emit event 'name'. FlattenForTUI spreads producer fields verbatim with no rename layer, so in production the current event rendered '?' and an upcoming event fell through to "📅 Free" -- the Go->JSON->Python field-drift class (cf. weather bug #29). Part of issue #155; canonical side is the Go producer (consistent + fixtured + tested), matching how the weather/insights segments already work. Switch the renderer's four next_event/current title reads to name. Add tests/test_status_segments.py asserting a producer-shaped calendar fixture renders the event name (red before, green after). News-segment story 'title' is a distinct, correct field and is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e0d7729 commit e21f208

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

tui/hookwise_tui/tabs/status.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -490,23 +490,23 @@ def _render_segment(seg: str, cache: dict) -> str:
490490
except (ValueError, KeyError):
491491
pass
492492
suffix = f" (+{len(events) - 1} more)" if len(events) > 1 else ""
493-
return f"\U0001f4c5 {current.get('title', '?')}{ends_in}{suffix}"
494-
if not isinstance(next_event, dict) or not next_event.get("title"):
493+
return f"\U0001f4c5 {current.get('name', '?')}{ends_in}{suffix}"
494+
if not isinstance(next_event, dict) or not next_event.get("name"):
495495
return "\U0001f4c5 Free"
496496
try:
497497
start_ms = int(datetime.fromisoformat(next_event["start"].replace("Z", "+00:00")).timestamp() * 1000)
498498
except (ValueError, KeyError):
499-
return f"\U0001f4c5 {next_event['title']}"
499+
return f"\U0001f4c5 {next_event['name']}"
500500
now_ms = int(time.time() * 1000)
501501
diff_min = round((start_ms - now_ms) / 60000)
502502
other_count = len(events) - 1 if len(events) > 1 else 0
503503
more_suffix = f" (+{other_count} more)" if other_count > 0 else ""
504504
if diff_min < 5:
505-
text = f"\U0001f4c5 {next_event['title']} NOW"
505+
text = f"\U0001f4c5 {next_event['name']} NOW"
506506
elif diff_min < 15:
507-
text = f"\U0001f4c5 {next_event['title']} in {diff_min}min \u26a1"
507+
text = f"\U0001f4c5 {next_event['name']} in {diff_min}min \u26a1"
508508
elif diff_min <= 60:
509-
text = f"\U0001f4c5 {next_event['title']} in {diff_min}min"
509+
text = f"\U0001f4c5 {next_event['name']} in {diff_min}min"
510510
else:
511511
hours = round(diff_min / 60)
512512
text = f"\U0001f4c5 Free for {hours}h"

tui/tests/test_status_segments.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Cross-boundary tests for StatusTab._render_segment().
2+
3+
These bind the Python status-line renderer to the EXACT field names the Go
4+
feed producers emit (after FlattenForTUI spreads the envelope's data fields to
5+
the top level of each cache entry). The renderer reads producer field names
6+
directly -- there is no rename layer in data.py -- so a mismatch silently
7+
degrades a segment in production (the Go->JSON->Python class, cf. weather bug
8+
#29). See GitHub issue #155: calendar/project read names the producers never
9+
emit. Canonical side: the Go producer (consistent + fixtured + tested); the
10+
Python renderer adapts to it here.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from datetime import datetime, timedelta, timezone
16+
17+
from hookwise_tui.tabs.status import StatusTab
18+
19+
20+
class TestCalendarSegment:
21+
"""The calendar producer emits event 'name'; the renderer must read it."""
22+
23+
def test_current_event_renders_producer_name(self):
24+
# Mirrors feeds.CalendarTestFixture(): events carry 'name', not 'title'.
25+
cache = {
26+
"calendar": {
27+
"events": [
28+
{
29+
"name": "Standup",
30+
"start": "2026-03-07T10:30:00Z",
31+
"end": "2999-01-01T00:00:00Z", # far future => "is current"
32+
"all_day": False,
33+
"is_current": True,
34+
}
35+
],
36+
"next_event": {"name": "Standup", "start": "2026-03-07T10:30:00Z"},
37+
}
38+
}
39+
out = StatusTab._render_segment("calendar", cache)
40+
assert "Standup" in out, (
41+
"calendar must render the producer's event 'name'; reading 'title' "
42+
"yields '?' (issue #155)"
43+
)
44+
assert "?" not in out
45+
46+
def test_next_event_renders_producer_name_not_free(self):
47+
# No current event, one upcoming ~30min out (inside the <=60min naming
48+
# window) -> must show its name. Reading 'title' here returned "Free"
49+
# because next_event.get("title") was falsy (issue #155).
50+
soon = (datetime.now(timezone.utc) + timedelta(minutes=30)).strftime(
51+
"%Y-%m-%dT%H:%M:%SZ"
52+
)
53+
cache = {
54+
"calendar": {
55+
"events": [{"name": "Standup", "start": soon, "is_current": False}],
56+
"next_event": {"name": "Standup", "start": soon},
57+
}
58+
}
59+
out = StatusTab._render_segment("calendar", cache)
60+
assert "Standup" in out, (
61+
"next_event must render the producer's 'name'; reading 'title' falls "
62+
"through to 'Free' (issue #155)"
63+
)

0 commit comments

Comments
 (0)