diff --git a/src/et/cli.py b/src/et/cli.py index 412c1d2..877ba8e 100644 --- a/src/et/cli.py +++ b/src/et/cli.py @@ -777,8 +777,9 @@ def jira_log_time( for logged in all_results.logged: duration = format_duration(logged.seconds_logged) + summary_display = f" '{logged.summary}'" if logged.summary else "" typer.echo( - f"Logged {duration} to {_jira_ref_link(logged.issue_key)} " + f"Logged {duration} to {_jira_ref_link(logged.issue_key)}{summary_display} " f"(workspace {logged.workspace_index + 1})" ) if logged.tracker_reset: diff --git a/src/et/jira_time.py b/src/et/jira_time.py index 13f6003..dfd0147 100644 --- a/src/et/jira_time.py +++ b/src/et/jira_time.py @@ -22,7 +22,7 @@ from et import tracker, workspaces from et.config import EtConfig, JiraConfig, load_config -from et.jira import JiraError, create_worklog +from et.jira import JiraError, create_worklog, fetch_issue from et.jira_ref import jira_key_from_ref from et.tracker import TrackerError @@ -37,12 +37,19 @@ class JiraLogTimeError(RuntimeError): @dataclass(frozen=True) class LogTimeResult: - """Summary of what `log_time_for_current_workspace` did.""" + """Summary of what `log_time_for_current_workspace` did. + + `summary` is the Jira issue's summary text, best-effort fetched after + logging (empty string if not fetched, e.g. for + `log_time_for_current_workspace`/`log_manual_time_for_current_workspace`, + or if the fetch itself failed in `log_time_for_all_workspaces`). + """ workspace_index: int issue_key: str seconds_logged: int tracker_reset: bool + summary: str = "" @dataclass(frozen=True) @@ -286,6 +293,14 @@ def log_time_for_all_workspaces( ) continue + # Best-effort: the time was already logged above, so a failure here + # (e.g. a transient network error) shouldn't be treated as a skip — + # just fall back to an empty summary. + try: + summary = fetch_issue(jira_config, issue_key).summary + except JiraError: + summary = "" + if reset: timer["timeElapsed"] = 0 timer["running"] = False @@ -302,6 +317,7 @@ def log_time_for_all_workspaces( issue_key=issue_key, seconds_logged=seconds, tracker_reset=reset, + summary=summary, ) ) diff --git a/tests/test_cli.py b/tests/test_cli.py index b5715c3..dfb9f1f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -748,7 +748,11 @@ def test_jira_log_time_all_reports_logged_and_skipped_workspaces( mock_log_all.return_value = AllLogTimeResult( logged=[ LogTimeResult( - workspace_index=0, issue_key="ISD-321", seconds_logged=3600, tracker_reset=True + workspace_index=0, + issue_key="ISD-321", + seconds_logged=3600, + tracker_reset=True, + summary="Epic summary", ) ], skipped=[ @@ -763,8 +767,9 @@ def test_jira_log_time_all_reports_logged_and_skipped_workspaces( assert result.exit_code == 0 mock_log_all.assert_called_once_with(reset=True) - assert "Logged 1h 0m 0s to " in ( - result.stdout + assert ( + "Logged 1h 0m 0s to " + "'Epic summary'" in result.stdout ) assert "Reset tracker to 0" in result.stdout assert ( diff --git a/tests/test_jira_time.py b/tests/test_jira_time.py index 3c67132..34e9073 100644 --- a/tests/test_jira_time.py +++ b/tests/test_jira_time.py @@ -7,7 +7,7 @@ import pytest from et.config import EtConfig, JiraConfig, WorkspaceConfigEntry -from et.jira import JiraError +from et.jira import JiraError, JiraIssue from et.jira_time import ( JiraLogTimeError, log_manual_time_for_current_workspace, @@ -49,6 +49,10 @@ def _timer(workspace_id: int, elapsed: int, name: str = "ET-1") -> dict: } +def _issue(key: str, *, summary: str = "Some summary") -> JiraIssue: + return JiraIssue(key=key, summary=summary, priority="Medium", status="In Progress") + + @patch("et.jira_time.tracker.save_timers_with_reload") @patch("et.jira_time.create_worklog") @patch("et.jira_time.tracker.load_timers") @@ -339,12 +343,13 @@ def test_resolve_issue_key_falls_back_to_active_workspace_without_override(mock_ # --- log_time_for_all_workspaces ------------------------------------------- +@patch("et.jira_time.fetch_issue") @patch("et.jira_time.tracker.save_timers_with_reload") @patch("et.jira_time.create_worklog") @patch("et.jira_time.tracker.load_timers") @patch("et.jira_time.load_config") def test_log_all_logs_and_resets_every_linked_workspace( - mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers + mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers, mock_fetch_issue ): mock_load_config.return_value = _config( [ @@ -354,11 +359,13 @@ def test_log_all_logs_and_resets_every_linked_workspace( ] ) mock_load_timers.return_value = [_timer(0, 3600), _timer(2, 1800, name="ET-3")] + mock_fetch_issue.side_effect = lambda _jira_config, key: _issue(key, summary=f"{key} summary") result = log_time_for_all_workspaces(description="Weekly sync") assert [r.issue_key for r in result.logged] == ["ISD-321", "ISD-654"] assert [r.seconds_logged for r in result.logged] == [3600, 1800] + assert [r.summary for r in result.logged] == ["ISD-321 summary", "ISD-654 summary"] assert all(r.tracker_reset for r in result.logged) assert result.skipped == [] @@ -373,6 +380,25 @@ def test_log_all_logs_and_resets_every_linked_workspace( assert mock_save_timers.call_count == 2 +@patch("et.jira_time.fetch_issue", side_effect=JiraError("summary fetch failed")) +@patch("et.jira_time.tracker.save_timers_with_reload") +@patch("et.jira_time.create_worklog") +@patch("et.jira_time.tracker.load_timers") +@patch("et.jira_time.load_config") +def test_log_all_falls_back_to_empty_summary_when_fetch_fails( + mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers, mock_fetch_issue +): + mock_load_config.return_value = _config( + [WorkspaceConfigEntry(name="ISD-321", ref="jira:ISD-321")] + ) + mock_load_timers.return_value = [_timer(0, 3600)] + + result = log_time_for_all_workspaces() + + assert result.logged[0].issue_key == "ISD-321" + assert result.logged[0].summary == "" + + @patch("et.jira_time.tracker.save_timers_with_reload") @patch("et.jira_time.create_worklog") @patch("et.jira_time.tracker.load_timers") @@ -415,12 +441,13 @@ def test_log_all_skips_workspace_with_no_timer( mock_save_timers.assert_not_called() +@patch("et.jira_time.fetch_issue") @patch("et.jira_time.tracker.save_timers_with_reload") @patch("et.jira_time.create_worklog", side_effect=JiraError("boom")) @patch("et.jira_time.tracker.load_timers") @patch("et.jira_time.load_config") def test_log_all_skips_workspace_whose_jira_call_fails_but_logs_the_rest( - mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers + mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers, mock_fetch_issue ): mock_load_config.return_value = _config( [ @@ -430,6 +457,7 @@ def test_log_all_skips_workspace_whose_jira_call_fails_but_logs_the_rest( ) timers = [_timer(0, 3600), _timer(1, 1800, name="ET-2")] mock_load_timers.return_value = timers + mock_fetch_issue.return_value = _issue("ISD-654") def create_worklog_side_effect(_jira_config, key, *_args, **_kwargs): if key == "ISD-321": @@ -473,17 +501,19 @@ def test_log_all_ignores_workspaces_with_no_linked_issue( mock_create_worklog.assert_not_called() +@patch("et.jira_time.fetch_issue") @patch("et.jira_time.tracker.save_timers_with_reload") @patch("et.jira_time.create_worklog") @patch("et.jira_time.tracker.load_timers") @patch("et.jira_time.load_config") def test_log_all_does_not_reset_when_disabled( - mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers + mock_load_config, mock_load_timers, mock_create_worklog, mock_save_timers, mock_fetch_issue ): mock_load_config.return_value = _config( [WorkspaceConfigEntry(name="ISD-321", ref="jira:ISD-321")] ) mock_load_timers.return_value = [_timer(0, 3600)] + mock_fetch_issue.return_value = _issue("ISD-321") result = log_time_for_all_workspaces(reset=False)