feat(stories): add stories subcommand

- Add stories subcommand listing stories worked on, grouped by project
- Preserve story_raw in parser row dicts alongside the stripped story,
  so markdown links are available for display
- print_stories() filters to rows with a non-empty story field,
  deduplicates by stripped story text (preferring the linked version),
  sums hours per story, and outputs an indented Markdown list
- Project names resolved through project_map (same as csv/summary)
- -w/--weekly flag aggregates stories across the full week
- Add tests for print_stories covering deduplication, link preservation,
  grouping, empty rows, and story-less row exclusion
- Fix flex daily target in status: use projected hours per prior day
  rather than fixed 8h when computing remaining hours for today
This commit is contained in:
Jef Roosens 2026-05-26 10:31:35 +02:00
parent f372a691d4
commit 2d60624e0e
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
5 changed files with 206 additions and 17 deletions

View file

@ -3,7 +3,7 @@ import io
import pytest
from timesheets.output import print_summary, write_csv
from timesheets.output import print_stories, print_summary, write_csv
# ---------------------------------------------------------------------------
# Shared fixtures
@ -111,3 +111,82 @@ class TestPrintSummary:
assert "01:00" in out
assert "00:30" in out
assert "00:15" in out
# ---------------------------------------------------------------------------
# print_stories
# ---------------------------------------------------------------------------
def _story_row(project, story, story_raw, hours):
return {
"project": project,
"story": story,
"story_raw": story_raw,
"note": "",
"duration_hours": hours,
}
class TestPrintStories:
def test_basic_output(self, capsys):
rows = [_story_row("bugs", "ticket 1", "ticket 1", 1.0)]
print_stories(rows)
out = capsys.readouterr().out
assert "- bugs" in out
assert " - ticket 1" in out
assert "01:00" in out
def test_rows_without_story_excluded(self, capsys):
rows = [
_story_row("bugs", "ticket 1", "ticket 1", 1.0),
{
"project": "scrum",
"story": "",
"story_raw": "",
"note": "dsu",
"duration_hours": 0.25,
},
]
print_stories(rows)
out = capsys.readouterr().out
assert "scrum" not in out
assert "dsu" not in out
def test_deduplication_sums_hours(self, capsys):
rows = [
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
]
print_stories(rows)
out = capsys.readouterr().out
assert out.count("ticket 1") == 1
assert "01:00" in out
def test_markdown_link_preserved(self, capsys):
rows = [
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
_story_row("bugs", "ticket 1", "[ticket 1](:/abc123)", 0.5),
]
print_stories(rows)
out = capsys.readouterr().out
assert "[ticket 1](:/abc123)" in out
def test_grouped_by_project(self, capsys):
rows = [
_story_row("bugs", "ticket 1", "ticket 1", 1.0),
_story_row("rate", "story A", "story A", 2.0),
]
print_stories(rows)
out = capsys.readouterr().out
assert "- bugs" in out
assert "- rate" in out
bugs_line = next(l for l in out.splitlines() if "bugs" in l)
rate_line = next(l for l in out.splitlines() if "rate" in l)
assert not bugs_line.startswith(" ") # top-level
assert not rate_line.startswith(" ")
def test_empty_rows(self, capsys):
print_stories([])
out = capsys.readouterr().out
assert out == ""