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
3 changes: 2 additions & 1 deletion src/et/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions src/et/jira_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -302,6 +317,7 @@ def log_time_for_all_workspaces(
issue_key=issue_key,
seconds_logged=seconds,
tracker_reset=reset,
summary=summary,
)
)

Expand Down
11 changes: 8 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand All @@ -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 <https://example.atlassian.net/browse/ISD-321|jira:ISD-321>" in (
result.stdout
assert (
"Logged 1h 0m 0s to <https://example.atlassian.net/browse/ISD-321|jira:ISD-321> "
"'Epic summary'" in result.stdout
)
assert "Reset tracker to 0" in result.stdout
assert (
Expand Down
38 changes: 34 additions & 4 deletions tests/test_jira_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
[
Expand All @@ -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 == []

Expand All @@ -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")
Expand Down Expand Up @@ -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(
[
Expand All @@ -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":
Expand Down Expand Up @@ -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)

Expand Down
Loading