diff --git a/analyzer/reporter.py b/analyzer/reporter.py index b381f45..d19af66 100644 --- a/analyzer/reporter.py +++ b/analyzer/reporter.py @@ -23,7 +23,7 @@ AGENT_LOADERS = {"claude-code": claude, "codex": codex} AGENT_NAMES = {"claude-code": "Claude Code", "codex": "Codex"} -PERSONA_DAYS_BY_PERIOD = {"today": 1, "week": 7, "month": 30, "all": 3650} +PERSONA_DAYS_BY_PERIOD = {"today": 1, "week": 7, "last7": 7, "month": 30, "all": 3650} def _entry_date(entry: UsageEntry) -> date: @@ -38,6 +38,8 @@ def _period_bounds(period: str, today: date) -> tuple[date | None, date]: return today, today if period == "week": return today - timedelta(days=today.weekday()), today + if period == "last7": + return today - timedelta(days=6), today if period == "month": return today.replace(day=1), today if period == "all": @@ -167,7 +169,7 @@ def _build_comparison( date_from: date, date_to: date, ) -> dict[str, Any]: - if period not in {"week", "month"}: + if period not in {"week", "last7", "month"}: return _empty_comparison(period) total_days = (date_to - date_from).days + 1 @@ -204,7 +206,7 @@ def _build_comparison( def build_report_data(agents: list[AgentInfo], period: str = "month") -> dict[str, Any]: """ - period: "today" | "week" | "month" | "all" + period: "today" | "week" | "last7" | "month" | "all" 回傳 dict,包含: period_label: str date_from: str @@ -219,13 +221,13 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> dict[st today = datetime.now().astimezone().date() date_from, date_to = _period_bounds(period, today) hours_back = 0 if date_from is None else ((date_to - date_from).days + 2) * 24 - if date_from is not None and period in {"week", "month"}: + if date_from is not None and period in {"week", "last7", "month"}: total_days_for_comparison = (date_to - date_from).days + 1 prev_date_from = date_from - timedelta(days=total_days_for_comparison) hours_back = ((date_to - prev_date_from).days + 2) * 24 raw_entries: list[UsageEntry] = [] - use_recent_codex = period in {"today", "week", "month"} + use_recent_codex = period in {"today", "week", "last7", "month"} for agent in agents: raw_entries.extend( _load_agent_entries(agent, hours_back, use_recent_codex=use_recent_codex) @@ -337,6 +339,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> dict[st }) return { + "period": period, "period_label": f"{date_from.isoformat()} -> {date_to.isoformat()}", "date_from": date_from.isoformat(), "date_to": date_to.isoformat(), diff --git a/menubar.py b/menubar.py index 9636944..96e5a87 100644 --- a/menubar.py +++ b/menubar.py @@ -1408,7 +1408,7 @@ def _analysis_period_from_project_range(project_range: str) -> str: if project_range == "1d": return "today" if project_range == "7d": - return "week" + return "last7" if project_range == "30d": return "last30" if project_range == "all": diff --git a/tests/test_analyzer_pipeline.py b/tests/test_analyzer_pipeline.py index 27ec6ef..d60014f 100644 --- a/tests/test_analyzer_pipeline.py +++ b/tests/test_analyzer_pipeline.py @@ -188,7 +188,7 @@ def fake_generate_analysis_report( def test_analysis_period_from_project_range() -> None: assert menubar._analysis_period_from_project_range("1d") == "today" - assert menubar._analysis_period_from_project_range("7d") == "week" + assert menubar._analysis_period_from_project_range("7d") == "last7" assert menubar._analysis_period_from_project_range("30d") == "last30" assert menubar._analysis_period_from_project_range("all") == "all" @@ -336,6 +336,88 @@ def fake_load_agent_entries( } +def test_report_last7_includes_previous_period_comparison(monkeypatch: Any) -> None: + class FixedDateTime: + @staticmethod + def now() -> datetime: + return datetime(2026, 5, 21, 12, tzinfo=UTC) + + agent = AgentInfo("codex", "Codex", "~/.codex", True) + entries = [ + UsageEntry( + timestamp=datetime(2026, 5, 8, tzinfo=UTC), + session_id="prev-1", + message_id="prev-1", + request_id="", + model="gpt-5-mini", + input_tokens=100, + output_tokens=0, + cache_creation_tokens=0, + cache_read_tokens=0, + cost_usd=1.0, + project="old", + agent_id="codex", + ), + UsageEntry( + timestamp=datetime(2026, 5, 14, tzinfo=UTC), + session_id="prev-2", + message_id="prev-2", + request_id="", + model="gpt-5-codex", + input_tokens=300, + output_tokens=0, + cache_creation_tokens=0, + cache_read_tokens=0, + cost_usd=3.0, + project="usage", + agent_id="codex", + ), + UsageEntry( + timestamp=datetime(2026, 5, 15, tzinfo=UTC), + session_id="cur-1", + message_id="cur-1", + request_id="", + model="gpt-5-codex", + input_tokens=600, + output_tokens=0, + cache_creation_tokens=0, + cache_read_tokens=0, + cost_usd=6.0, + project="usage", + agent_id="codex", + ), + ] + calls: dict[str, int] = {} + + def fake_load_agent_entries( + received_agent: AgentInfo, + hours_back: int = 0, + *, + use_recent_codex: bool = False, + ) -> list[UsageEntry]: + assert received_agent == agent + assert use_recent_codex is True + calls["hours_back"] = hours_back + return entries + + monkeypatch.setattr(reporter, "datetime", FixedDateTime) + monkeypatch.setattr(reporter, "_load_agent_entries", fake_load_agent_entries) + monkeypatch.setattr("analyzer.reporter.subscription.load_subscriptions", lambda: []) + + data = reporter.build_report_data([agent], "last7") + + assert calls == {"hours_back": 360} + assert data["summary"]["total_tokens"] == 600 + assert data["comparison"] == { + "period": "last7", + "has_prev": True, + "prev_tokens": 400, + "prev_cost": 4.0, + "prev_projects": ["old", "usage"], + "prev_model_share": {"gpt-5-codex": 75.0, "gpt-5-mini": 25.0}, + } + + def test_build_report_data_includes_serialized_persona(monkeypatch: Any) -> None: histogram = [0] * 24 histogram[9] = 3 @@ -354,7 +436,7 @@ def fake_load_profile(days_back: int = 30) -> persona_loader.PersonaProfile: monkeypatch.setattr("analyzer.reporter.persona_loader.load_profile", fake_load_profile) monkeypatch.setattr("analyzer.reporter.subscription.load_subscriptions", lambda: []) - data = reporter.build_report_data([], "week") + data = reporter.build_report_data([], "last7") assert calls == [7] assert data["persona"] == { diff --git a/tests/test_usage_cli.py b/tests/test_usage_cli.py index 291bb95..6730257 100644 --- a/tests/test_usage_cli.py +++ b/tests/test_usage_cli.py @@ -167,6 +167,7 @@ def test_main_codex_warning_checks_only_codex_setup(monkeypatch: pytest.MonkeyPa [ (["usage", "report"], "last30"), (["usage", "report", "--last30"], "last30"), + (["usage", "report", "--last7"], "last7"), (["usage", "report", "--all"], "all"), ], ) @@ -254,6 +255,7 @@ def test_parse_report_args_defaults_to_last30() -> None: ("flag", "expected_period"), [ ("--today", "today"), + ("--last7", "last7"), ("--week", "week"), ("--month", "month"), ("--all", "all"), diff --git a/usage_cli.py b/usage_cli.py index fc89188..50f5c5f 100644 --- a/usage_cli.py +++ b/usage_cli.py @@ -60,7 +60,7 @@ def _load_codex_rate_limits() -> RateLimits | None: "output": ("output_tokens", True), } -REPORT_HELP = """Usage: usage report [--last30|--all|--today|--week|--month] [--out PATH] +REPORT_HELP = """Usage: usage report [--last30|--all|--today|--last7|--week|--month] [--out PATH] Generate an HTML usage report. @@ -68,6 +68,7 @@ def _load_codex_rate_limits() -> RateLimits | None: --last30 Include the last 30 days (default) --all Include all usage data --today Include today only + --last7 Include the last 7 days --week Include this week --month Include this month --out PATH Save to a specific path @@ -110,6 +111,8 @@ def _parse_report_args(args: list[str]) -> tuple[str, str | None, bool]: period = "last30" elif arg == "--today": period = "today" + elif arg == "--last7": + period = "last7" elif arg == "--week": period = "week" elif arg == "--month":