Skip to content

Commit d04b63d

Browse files
authored
Merge pull request #882 from microsasa/fix/879-render-detail-test-coverage-16aec4ca8168955f
test: add coverage for render_detail private helpers (#879)
2 parents cc722f6 + e8614a4 commit d04b63d

1 file changed

Lines changed: 156 additions & 12 deletions

File tree

tests/copilot_usage/test_render_detail.py

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010
from rich.console import Console
1111

12+
from copilot_usage._formatting import MAX_CONTENT_LEN
1213
from copilot_usage.models import (
1314
CodeChanges,
1415
EventType,
@@ -22,7 +23,11 @@
2223
ToolTelemetry,
2324
)
2425
from copilot_usage.render_detail import (
26+
_build_event_details,
27+
_event_type_label,
2528
_extract_tool_name,
29+
_format_relative_time,
30+
_render_active_period,
2631
_render_code_changes,
2732
_render_recent_events,
2833
_render_shutdown_cycles,
@@ -508,8 +513,6 @@ class TestBuildEventDetailsToolRequests:
508513
def test_tool_only_turn_shows_tool_names(self) -> None:
509514
"""ASSISTANT_MESSAGE with content='', outputTokens=0, and two
510515
toolRequests must render both tool names."""
511-
from copilot_usage.render_detail import _build_event_details
512-
513516
ev = SessionEvent(
514517
type=EventType.ASSISTANT_MESSAGE,
515518
data={
@@ -529,8 +532,6 @@ def test_tool_only_turn_shows_tool_names(self) -> None:
529532
def test_mixed_turn_shows_tokens_and_tool(self) -> None:
530533
"""ASSISTANT_MESSAGE with content, outputTokens, and one
531534
toolRequest must render token info and the tool name."""
532-
from copilot_usage.render_detail import _build_event_details
533-
534535
ev = SessionEvent(
535536
type=EventType.ASSISTANT_MESSAGE,
536537
data={
@@ -548,8 +549,6 @@ def test_mixed_turn_shows_tokens_and_tool(self) -> None:
548549

549550
def test_no_tools_unchanged(self) -> None:
550551
"""ASSISTANT_MESSAGE without toolRequests must behave as before."""
551-
from copilot_usage.render_detail import _build_event_details
552-
553552
ev = SessionEvent(
554553
type=EventType.ASSISTANT_MESSAGE,
555554
data={
@@ -564,8 +563,6 @@ def test_no_tools_unchanged(self) -> None:
564563

565564
def test_truncation_applied_to_long_tool_list(self) -> None:
566565
"""When the joined tool names exceed 60 chars, truncation applies."""
567-
from copilot_usage.render_detail import _build_event_details
568-
569566
long_names = [
570567
{"name": f"very_long_tool_name_{i}", "toolCallId": f"t{i}"}
571568
for i in range(10)
@@ -584,8 +581,6 @@ def test_truncation_applied_to_long_tool_list(self) -> None:
584581

585582
def test_empty_names_show_unknown(self) -> None:
586583
"""toolRequests present but all names empty must show '(unknown)'."""
587-
from copilot_usage.render_detail import _build_event_details
588-
589584
ev = SessionEvent(
590585
type=EventType.ASSISTANT_MESSAGE,
591586
data={
@@ -602,8 +597,6 @@ def test_empty_names_show_unknown(self) -> None:
602597

603598
def test_singular_label_based_on_displayed_names(self) -> None:
604599
"""When two toolRequests exist but only one has a name, use 'tool'."""
605-
from copilot_usage.render_detail import _build_event_details
606-
607600
ev = SessionEvent(
608601
type=EventType.ASSISTANT_MESSAGE,
609602
data={
@@ -662,3 +655,154 @@ def test_multi_model_shutdown_via_full_render(self) -> None:
662655
row = next(line for line in output.splitlines() if "2025-01-01 01:00" in line)
663656
assert re.search(r"\b7\b", row) # total API requests = 3 + 4
664657
assert re.search(r"\b800\b", row) # total output tokens = 500 + 300
658+
659+
660+
# ---------------------------------------------------------------------------
661+
# _format_relative_time — direct unit tests (issue #879)
662+
# ---------------------------------------------------------------------------
663+
664+
665+
class TestFormatRelativeTime:
666+
"""Direct unit tests covering all branches of _format_relative_time."""
667+
668+
def test_sub_hour_formats_as_m_ss(self) -> None:
669+
"""timedelta(minutes=4, seconds=7) → '+4:07'."""
670+
assert _format_relative_time(timedelta(minutes=4, seconds=7)) == "+4:07"
671+
672+
def test_over_hour_formats_as_h_mm_ss(self) -> None:
673+
"""timedelta(hours=1, minutes=2, seconds=3) → '+1:02:03'."""
674+
assert (
675+
_format_relative_time(timedelta(hours=1, minutes=2, seconds=3))
676+
== "+1:02:03"
677+
)
678+
679+
def test_negative_delta_clamped_to_zero(self) -> None:
680+
"""Negative timedelta must clamp to '+0:00', never a negative string."""
681+
assert _format_relative_time(timedelta(seconds=-10)) == "+0:00"
682+
683+
def test_zero_delta(self) -> None:
684+
"""Zero timedelta → '+0:00'."""
685+
assert _format_relative_time(timedelta()) == "+0:00"
686+
687+
def test_exactly_one_hour(self) -> None:
688+
"""Exactly 1h boundary triggers the hours branch."""
689+
assert _format_relative_time(timedelta(hours=1)) == "+1:00:00"
690+
691+
692+
# ---------------------------------------------------------------------------
693+
# _render_active_period — direct unit tests (issue #879)
694+
# ---------------------------------------------------------------------------
695+
696+
697+
class TestRenderActivePeriod:
698+
"""Direct unit tests for _render_active_period covering active / inactive."""
699+
700+
def test_active_session_renders_panel(self) -> None:
701+
"""Active session must render an 'Active Period' panel with stats."""
702+
summary = SessionSummary(
703+
session_id="active-test",
704+
is_active=True,
705+
model_calls=3,
706+
user_messages=2,
707+
active_model_calls=3,
708+
active_user_messages=2,
709+
active_output_tokens=1000,
710+
)
711+
buf, console = _buf_console()
712+
_render_active_period(summary, target_console=console)
713+
output = _strip_ansi(buf.getvalue())
714+
assert "Active Period" in output
715+
assert "3 model calls" in output
716+
assert "2 user messages" in output
717+
718+
def test_inactive_session_produces_no_output(self) -> None:
719+
"""Inactive session → returns immediately, no output."""
720+
summary = SessionSummary(session_id="inactive-test", is_active=False)
721+
buf, console = _buf_console()
722+
_render_active_period(summary, target_console=console)
723+
assert buf.getvalue() == ""
724+
725+
726+
class TestRenderSessionDetailActivePeriod:
727+
"""Integration test: render_session_detail with is_active=True must
728+
render the Active Period panel (issue #879)."""
729+
730+
def test_active_session_shows_active_period_panel(self) -> None:
731+
"""render_session_detail with is_active=True must include the
732+
Active Period panel in its output."""
733+
summary = SessionSummary(
734+
session_id="active-e2e",
735+
start_time=datetime(2026, 4, 1, 10, 0, 0, tzinfo=UTC),
736+
is_active=True,
737+
model_calls=5,
738+
user_messages=3,
739+
active_model_calls=5,
740+
active_user_messages=3,
741+
active_output_tokens=2000,
742+
)
743+
ev = SessionEvent(
744+
type=EventType.USER_MESSAGE,
745+
timestamp=datetime(2026, 4, 1, 10, 5, 0, tzinfo=UTC),
746+
data={"content": "hello"},
747+
)
748+
buf, console = _buf_console()
749+
render_session_detail([ev], summary, target_console=console)
750+
output = _strip_ansi(buf.getvalue())
751+
assert "Active Period" in output
752+
753+
754+
# ---------------------------------------------------------------------------
755+
# _event_type_label — parametrized unit tests (issue #879)
756+
# ---------------------------------------------------------------------------
757+
758+
759+
class TestEventTypeLabel:
760+
"""Parametrized test for _event_type_label covering every labelled
761+
EventType case and the wildcard branch."""
762+
763+
@pytest.mark.parametrize(
764+
("event_type", "expected_text"),
765+
[
766+
pytest.param(EventType.USER_MESSAGE, "user message", id="user-message"),
767+
pytest.param(EventType.ASSISTANT_MESSAGE, "assistant", id="assistant"),
768+
pytest.param(EventType.TOOL_EXECUTION_COMPLETE, "tool", id="tool-complete"),
769+
pytest.param(EventType.TOOL_EXECUTION_START, "tool start", id="tool-start"),
770+
pytest.param(EventType.ASSISTANT_TURN_START, "turn start", id="turn-start"),
771+
pytest.param(EventType.ASSISTANT_TURN_END, "turn end", id="turn-end"),
772+
pytest.param(EventType.SESSION_START, "session start", id="session-start"),
773+
pytest.param(EventType.SESSION_SHUTDOWN, "session end", id="session-end"),
774+
pytest.param(
775+
"UNKNOWN_FUTURE_TYPE", "UNKNOWN_FUTURE_TYPE", id="wildcard-branch"
776+
),
777+
],
778+
)
779+
def test_label_text(self, event_type: str, expected_text: str) -> None:
780+
"""Label plain text must match the expected string."""
781+
label = _event_type_label(event_type)
782+
assert label.plain == expected_text
783+
784+
785+
# ---------------------------------------------------------------------------
786+
# _build_event_details — USER_MESSAGE branch (issue #879)
787+
# ---------------------------------------------------------------------------
788+
789+
790+
class TestBuildEventDetailsUserMessage:
791+
"""Tests for the USER_MESSAGE branch of _build_event_details."""
792+
793+
def test_content_returned(self) -> None:
794+
"""Short content is returned as-is."""
795+
ev = SessionEvent(type=EventType.USER_MESSAGE, data={"content": "hello"})
796+
assert _build_event_details(ev) == "hello"
797+
798+
def test_long_content_truncated(self) -> None:
799+
"""Content exceeding MAX_CONTENT_LEN must be truncated with '…'."""
800+
ev = SessionEvent(type=EventType.USER_MESSAGE, data={"content": "x" * 300})
801+
detail = _build_event_details(ev)
802+
assert detail.endswith("…")
803+
assert len(detail) <= MAX_CONTENT_LEN
804+
805+
def test_empty_content_returns_empty_string(self) -> None:
806+
"""Empty content → empty string."""
807+
ev = SessionEvent(type=EventType.USER_MESSAGE, data={"content": ""})
808+
assert _build_event_details(ev) == ""

0 commit comments

Comments
 (0)