diff --git a/AGENTS.md b/AGENTS.md index 16579ae..c3e55f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ - Use `./start.sh` as the primary entry point for setup, one-off reports, and email-report configuration. - The project uses `pyproject.toml` for all dependency declarations. Do not create a `requirements.txt` unless a specific tool requires it. - Do not print, commit, or store real GitHub tokens, raw private API responses, or generated billing reports. +- **NEVER read or cat** `.env.email-report` or `.sonar_cloud_token` — these files contain sensitive credentials. - Tests should use fake tokens, mocks, and fixtures rather than live GitHub API calls. - Optional live checks must be gated behind an explicit environment variable. diff --git a/CHANGELOG.md b/CHANGELOG.md index 87302b3..9449fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ This project follows the structure from Keep a Changelog and intends to use Sema ### Fixed +- **Email report forecast visibility split** ([plan](docs/superpowers/plans/archived/2026-08-10-fix-email-forecast-visibility-split.md)): `build_report_data()` now calls `attach_actions_visibility_split()` so the email report's Actions forecast, key insights, warning threshold, and text/HTML actions sub-line all use private-only minutes against the free-tier limit. When `include_consumers=True`, the consumers' existing per-repo rows are reused (zero extra API calls); when `include_consumers=False`, `fetch_repo_actions_table()` provides the per-repo data. `needs_repos` now includes `include_actions` so repos are always available for the split. - **SonarCloud reliability (S6466):** `get_key_insights` in `report_data.py` adds an explicit `None`-guard (`or []`) on `by_minutes` before indexing, satisfying SonarCloud's S6466 check; new test covers `by_minutes: None`. - **CodeRabbit follow-ups (PR #10):** Soft-fail `workflow_breakdown` fetch on `RuntimeError` (email + legacy paths) so partial reports still render; skip redundant private concentration recommendations when private top-2 matches overall top-2; share `repo_label` / `WORKFLOW_MINUTES_REQUEST_HEADROOM` / public `parse_iso_datetime`; narrow workflow-name-map soft-fail to `RuntimeError`; normalize non-UTC ISO offsets to UTC in `parse_iso_datetime` so expiry/retention day math stays calendar-stable. - **SonarCloud quality gate (PR #10):** Safer list indexing for private consumer findings and workflow breakdown (`S6466`); reduced cognitive complexity in `_format_consumers_section` and `_repo_rows`; deduplicated HTML `` literals; consolidated repeated consumer test fixtures into `tests/_consumer_fixtures.py`. diff --git a/TO_DO.md b/TO_DO.md index bd4e946..36d6817 100644 --- a/TO_DO.md +++ b/TO_DO.md @@ -29,6 +29,8 @@ - [ ] Refactor `src/github_usage/setup_config.py` (507 lines, over the 500-line limit per `scripts/check-sizes`). Extract a focused submodule — e.g. profile schema/loading (`load_report_profiles`, `find_profile`, `ensure_profiles`, `_default_profile`) and/or the TOML writer helpers (`_emit_*_block`, `write_config`) — to bring the file back under the threshold. Also watch `setup_wizard.py` (461 lines) and the `_manage_profiles()`/`_run_email_report()` functions, which are approaching their limits. - [ ] Rename internal `legacy_*` modules/symbols to “local full report” naming (`legacy_report_data` → e.g. `local_report_data`, cache `kind="legacy"`, CLI/TUI internals, tests). User-facing copy already says “local full report”; this is the code rename. Keep a thin `legacy` compatibility shim if external imports still need it. +- [ ] Add tests to raise overall src coverage from the current ~76% to 80%. Update `scripts/coverage-check` default from `COVERAGE_TOTAL_MIN=75` to `80` once achieved. + ## Configuration & Setup - [ ] Write Windows-compatible PowerShell versions of all scripts (setup, check, smoke, docs-check, etc.). diff --git a/docs/superpowers/plans/archived/2026-08-10-fix-email-forecast-visibility-split.md b/docs/superpowers/plans/archived/2026-08-10-fix-email-forecast-visibility-split.md new file mode 100644 index 0000000..8ad3423 --- /dev/null +++ b/docs/superpowers/plans/archived/2026-08-10-fix-email-forecast-visibility-split.md @@ -0,0 +1,87 @@ +# Fix: Email report forecast should use private-only quota inputs + +> **Status:** COMPLETE + +## Problem + +The email report forecast projects total Actions minutes (private + public) against the 2,000-minute free-tier limit. Only private repos consume quota — public repos are free. The legacy terminal report handles this correctly; the email report does not. + +The consumers section already shows a private/public breakdown (computed independently from per-repo data in `get_repo_consumers()`), but the **account-level** visibility split is missing from `report["actions"]`. This affects: + +| Section | What it reads | Impact | +|---|---|---| +| Forecast scope note | `"public_minutes" in forecast` | No "(private repos — quota-counted)" label | +| Key Insight 1 | `actions["private_minutes_percent"]` | Insight silently dropped when >=100% | +| Warning threshold | Prefers `private_minutes_percent`, falls back to `minutes_percent` | Evaluates against total (private+public) | +| Text actions sub-line | `"public_minutes" in actions` | Private/public breakdown not shown | + +## Root Cause + +`report_data.build_report_data()` (the email path) fetches account-level billing via `get_actions_usage()` which returns combined private+public minutes, then never calls `attach_actions_visibility_split()` to reconcile the per-repo split into account-level private/public aggregates. + +The legacy path (`legacy_report_data.build_legacy_report_data()`) correctly calls `attach_actions_visibility_split()` at line 326, which adds `private_minutes`, `public_minutes`, `private_minutes_percent`, etc. to `report["actions"]`. + +## Files Involved + +- `src/github_usage/report_data.py` — `build_report_data()` (line 319) — **missing the split call** +- `src/github_usage/legacy_report_data.py` — `build_legacy_report_data()` (line 326) — **has the call (reference)** +- `src/github_usage/usage_split.py` — `attach_actions_visibility_split()` (line 282) — requires `repo_actions` list +- `src/github_usage/report_forecast_data.py` — `_private_quota_inputs()` (line 17) — fallback logic when split is absent +- `src/github_usage/report_data.py` — `get_key_insights()` (line 203) — insight 1 depends on `private_minutes_percent` +- `src/github_usage/report_data.py` — `get_warning_state()` (line 155) — threshold prefers `private_minutes_percent` +- `src/github_usage/email_report_html.py` — forecast rendering (line 345) — checks `"public_minutes" in forecast` +- `src/github_usage/email_report_text.py` — actions section (line 47) — checks `"public_minutes" in actions` +- `src/github_usage/report_optional.py` — `get_repo_consumers()` (line 25) — already fetches per-repo data; needs to expose raw rows +- `src/github_usage/report_actions.py` — `fetch_repo_actions_table()` (line 172) — returns `(rows, errors)` tuple, used by legacy path + +## Recommendation + +Add a call to `attach_actions_visibility_split()` in `build_report_data()` after `_fetch_sections()` returns and before `get_key_insights()` / `get_warning_state()`. The split needs per-repo Actions billing data (`repo_actions`) as its second argument. + +### Approach: hybrid — reuse consumers rows when available, fetch separately otherwise + +Two code paths feed the split, depending on whether consumers are enabled: + +1. **`include_consumers=True`** — `get_repo_consumers()` already calls `get_actions_per_repo()` for every repo (`report_optional.py:35`) and builds rows with `repo`, `minutes`, `storage_avg_mb`, and `visibility` — the same shape `attach_actions_visibility_split()` needs. Extend `get_repo_consumers()` to return its raw rows under a `_raw_rows` key (or a dedicated public key), then pass them directly to `attach_actions_visibility_split()`. This avoids duplicating every per-repo API call. + +2. **`include_consumers=False`** — `get_repo_consumers()` is not called, so no rows are available. Call `fetch_repo_actions_table(api, repos)` from `report_actions.py:172`, which returns `(rows, errors)` — the same function the legacy path uses. + +### Prerequisite: ensure `repos` is populated when `include_actions=True` + +Currently `build_report_data()` only fetches repos when `needs_repos = include_consumers or include_artifact_storage or include_release_assets` (line 338). When `include_actions=True` alone, `repos` is `[]` and the split has nothing to iterate. Fix: add `include_actions` to `needs_repos` so repos are always available when the split is needed. (The legacy path always fetches repos, so this is consistent.) + +### Recommended call-site ordering + +`build_report_data()` computes `report["insights"]` and `report["warnings"]` immediately after `_fetch_sections()` returns (lines 395-396), and both `get_key_insights()` (reads `private_minutes_percent`) and `get_warning_state()` (prefers `private_minutes_percent`, falls back to `minutes_percent`) consume the split keys. The `attach_actions_visibility_split()` call must therefore be inserted **after** `actions` is populated by `_fetch_sections()` and **before** the insights/warnings lines — i.e., between line 393 and line 395 in `report_data.py`. Placing it inside `_fetch_sections()` is acceptable too, but it must run before those two consumers read `report["actions"]`. Failing to honor this ordering silently leaves the bug in place even after "adding the call." + +### Scope note: `include_actions=False` + +`attach_actions_visibility_split()` already no-ops when `report["actions"]` is `None` (usage_split.py:296-297), so the call is safe to add unconditionally. However, the per-repo Actions fetch (path 2 above) should be skipped when `include_actions=False`, since the split has nothing to attach to. + +### API cost note + +When `include_consumers=True`, approach 1 reuses the consumers' existing per-repo calls — zero extra API cost. When `include_consumers=False`, approach 2 adds one API call per repo (same as the legacy path). For users with `max_repos=100`, this is up to 100 additional calls. This is acceptable and consistent with the legacy path's behavior. + +## Tasks + +- [x] Expand `needs_repos` in `build_report_data()` to include `include_actions` so repos are always fetched when the split is needed +- [x] Extend `get_repo_consumers()` in `report_optional.py` to return raw per-repo rows (e.g. `_raw_rows` key) for reuse by the split +- [x] Add `attach_actions_visibility_split()` call to `build_report_data()` in `report_data.py`, using consumers raw rows when available, falling back to `fetch_repo_actions_table()` when not +- [x] Verify `get_key_insights()` and `get_warning_state()` need no changes (they already handle the fallback correctly — the fix just ensures the split keys are present) +- [x] Add test: email report path with `include_consumers=True` reuses consumer rows for the split (no duplicate API calls) +- [x] Add test: email report path with `include_actions=True` and `include_consumers=False` uses `fetch_repo_actions_table()` for the split +- [x] Add test: `include_actions=False` skips the per-repo fetch entirely +- [x] Run `scripts/check` and `scripts/smoke` +- [x] Update CHANGELOG.md + +**Done:** 2026-08-10 — All tasks implemented and verified. `scripts/check` and `scripts/smoke` pass. + +## Verification + +1. Generate an email report and confirm the forecast section shows "(private repos — quota-counted)" label +2. Confirm Key Insight 1 appears when private minutes >= 100% +3. Confirm the text actions section shows the private/public sub-line +4. Confirm the warning threshold evaluates against private-only minutes +5. Confirm the consumers section still works correctly (it should be unaffected) +6. Confirm the email path with `include_actions=True` and `include_consumers=False` shows the split (exercises the `fetch_repo_actions_table()` fallback path) +7. Confirm no duplicate per-repo API calls when `include_consumers=True` (the consumers' existing calls are reused for the split) diff --git a/src/github_usage/report_data.py b/src/github_usage/report_data.py index 527c76a..1dc8447 100644 --- a/src/github_usage/report_data.py +++ b/src/github_usage/report_data.py @@ -5,6 +5,7 @@ from datetime import UTC, datetime from typing import Protocol +from .report_actions import fetch_repo_actions_table from .report_helpers import fmt_price, gb_hours_to_avg_mb, sanitize_item_amounts from .report_optional import ( estimate_api_request_count, @@ -13,7 +14,7 @@ get_repo_consumers, ) from .report_workflow_minutes import workflow_breakdown_for_top_private -from .usage_split import REPORT_SOURCES +from .usage_split import REPORT_SOURCES, attach_actions_visibility_split from .visibility import filter_repos_by_visibility, repo_visibility, visibility_label @@ -335,7 +336,9 @@ def build_report_data( errors = {} repos: list = [] truncated = False - needs_repos = include_consumers or include_artifact_storage or include_release_assets + needs_repos = ( + include_actions or include_consumers or include_artifact_storage or include_release_assets + ) if needs_repos: repos, truncated = _limited_repos(api, max_repos) repos = filter_repos_by_visibility( @@ -344,6 +347,7 @@ def build_report_data( core_limit, core_remaining = _rate_limit(api) api_estimate = estimate_api_request_count( repo_count=len(repos) + (1 if truncated else 0), + include_actions=include_actions, include_consumers=include_consumers, include_artifact_storage=include_artifact_storage, include_release_assets=include_release_assets, @@ -392,6 +396,21 @@ def build_report_data( runs_cache=runs_cache, ) + if include_actions and report.get("actions") is not None: + consumers_rows = None + if include_consumers and report.get("repo_consumers"): + consumers_rows = report["repo_consumers"].get("_raw_rows") + if consumers_rows is not None: + attach_actions_visibility_split( + report, consumers_rows, only_public=only_public, only_private=only_private + ) + else: + rows, fetch_errors = fetch_repo_actions_table(api, repos) + errors.update(fetch_errors) + attach_actions_visibility_split( + report, rows, only_public=only_public, only_private=only_private + ) + report["insights"] = get_key_insights(report) report["warnings"] = get_warning_state(report, warn_over) report["sources"] = dict(REPORT_SOURCES) diff --git a/src/github_usage/report_optional.py b/src/github_usage/report_optional.py index 59cbfe2..c692ee3 100644 --- a/src/github_usage/report_optional.py +++ b/src/github_usage/report_optional.py @@ -42,6 +42,7 @@ def get_repo_consumers(api, repos: list[dict], limit: int = 5, max_repos: int = "minutes": float(minutes), "gross": sum(float(item.get("grossAmount", 0.0)) for item in sku.values()), "storage_avg_mb": gb_hours_to_avg_mb(float(storage_gb_hours)), + "storage_gb_hours": float(storage_gb_hours), "visibility": repo_visibility(repo), } ) @@ -53,6 +54,9 @@ def get_repo_consumers(api, repos: list[dict], limit: int = 5, max_repos: int = **rankings, "errors": errors, "by_visibility": split_rows_by_visibility(rows, storage_key="storage_avg_mb", sku_key=None), + # Used by report_data.build_report_data() to pass per-repo rows to + # attach_actions_visibility_split() without duplicate API calls. + "_raw_rows": rows, } @@ -120,6 +124,7 @@ def get_release_asset_details(api, repos: list[dict], max_repos: int = 100) -> d def estimate_api_request_count( repo_count: int, + include_actions: bool, include_consumers: bool, include_artifact_storage: bool, include_release_assets: bool, @@ -129,7 +134,10 @@ def estimate_api_request_count( ) -> dict: """Estimate the number of additional API requests optional sections will make.""" repos_considered = min(repo_count, max_repos) - per_repo_options = sum([include_consumers, include_artifact_storage, include_release_assets]) + fallback_actions = include_actions and not include_consumers + per_repo_options = sum( + [include_consumers, include_artifact_storage, include_release_assets, fallback_actions] + ) estimated = repos_considered * per_repo_options if include_consumers: estimated += WORKFLOW_MINUTES_REQUEST_HEADROOM diff --git a/tests/test_report_data.py b/tests/test_report_data.py index f37a100..c9c7f3c 100644 --- a/tests/test_report_data.py +++ b/tests/test_report_data.py @@ -1,7 +1,104 @@ import unittest +from unittest import mock from tests._fakes import FakeAPI +_PRIVATE_REPO = { + "full_name": "octocat/private-repo", + "owner": {"login": "octocat"}, + "name": "private-repo", + "visibility": "private", +} + +_PUBLIC_REPO = { + "full_name": "octocat/public-repo", + "owner": {"login": "octocat"}, + "name": "public-repo", + "visibility": "public", +} + +_TWO_REPOS = (_PRIVATE_REPO, _PUBLIC_REPO) + +_PRIVATE_ROW = { + "repo": "octocat/private-repo", + "minutes": 1000.0, + "storage_gb_hours": 0.0, + "avg_mb": 0.0, + "gross": 0.0, + "sku": {}, + "visibility": "private", +} + +_PUBLIC_ROW = { + "repo": "octocat/public-repo", + "minutes": 500.0, + "storage_gb_hours": 0.0, + "avg_mb": 0.0, + "gross": 0.0, + "sku": {}, + "visibility": "public", +} + +_DEFAULT_FAKE_ROWS = (_PRIVATE_ROW, _PUBLIC_ROW) + +_DEFAULT_KWARGS = { + "include_actions": True, + "include_copilot": False, + "include_lfs": False, + "include_consumers": False, + "include_artifact_storage": False, + "include_release_assets": False, + "max_repos": 100, + "warn_over": None, +} + + +def _make_billing_api( + actions_items=None, + copilot_items=None, + premium_items=None, + lfs_items=None, + rate_limit=None, + pages=None, + omit=(), +): + """Return a FakeAPI pre-loaded with standard billing endpoints. + + Pass ``omit=("actions",)`` to exclude the Actions endpoint. + """ + responses = {} + default_rate = {"resources": {"core": {"limit": 5000, "remaining": 4900}}} + + if "actions" not in omit: + responses[ + ("GET", "/users/octocat/settings/billing/usage/summary", (("product", "Actions"),)) + ] = {"usageItems": actions_items or []} + + if "copilot" not in omit: + responses[ + ("GET", "/users/octocat/settings/billing/usage/summary", (("product", "Copilot"),)) + ] = {"usageItems": copilot_items or []} + responses[ + ( + "GET", + "/users/octocat/settings/billing/premium_request/usage", + (("product", "copilot"),), + ) + ] = {"usageItems": premium_items or []} + + if "lfs" not in omit: + responses[ + ("GET", "/users/octocat/settings/billing/usage/summary", (("product", "git_lfs"),)) + ] = {"usageItems": lfs_items or []} + + if "rate_limit" not in omit: + responses[("GET", "/rate_limit", ())] = rate_limit or default_rate + + return FakeAPI( + request_responses=responses, + pages_responses=pages or {}, + ) + class ReportDataTests(unittest.TestCase): def test_build_report_data_captures_partial_endpoint_failure(self): @@ -58,6 +155,7 @@ def test_estimate_api_request_count_respects_max_repos_and_options(self): estimate = estimate_api_request_count( repo_count=250, + include_actions=False, include_consumers=True, include_artifact_storage=True, include_release_assets=True, @@ -70,6 +168,32 @@ def test_estimate_api_request_count_respects_max_repos_and_options(self): self.assertEqual(estimate["estimated_incremental_requests"], 85) self.assertEqual(estimate["estimated_percent_of_remaining"], 85.0) + def test_estimate_includes_actions_fallback_when_consumers_disabled(self): + from github_usage.report_data import estimate_api_request_count + + estimate = estimate_api_request_count( + repo_count=10, + include_actions=True, + include_consumers=False, + include_artifact_storage=False, + include_release_assets=False, + max_repos=100, + ) + self.assertEqual(estimate["estimated_incremental_requests"], 10) + + def test_estimate_skips_actions_fallback_when_consumers_enabled(self): + from github_usage.report_data import estimate_api_request_count + + estimate = estimate_api_request_count( + repo_count=10, + include_actions=True, + include_consumers=True, + include_artifact_storage=False, + include_release_assets=False, + max_repos=100, + ) + self.assertEqual(estimate["estimated_incremental_requests"], 20) + def test_get_warning_state_handles_missing_monthly_costs(self): from github_usage.report_data import get_warning_state @@ -300,3 +424,151 @@ def test_rate_limit_handles_non_dict_response(self): ) self.assertEqual(_rate_limit(api), (None, None)) + + def test_consumers_enabled_reuses_rows_for_visibility_split(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api( + actions_items=[ + { + "sku": "actions_linux", + "unitType": "minutes", + "grossQuantity": 1500.0, + "grossAmount": 0.0, + "discountAmount": 0.0, + "netAmount": 0.0, + } + ], + pages={"/user/repos": _TWO_REPOS}, + ) + with ( + mock.patch( + "github_usage.report_optional.get_actions_per_repo", + side_effect=[ + (1000.0, 0.0, {"sku": {"grossAmount": 0.0}}), + (500.0, 0.0, {"sku": {"grossAmount": 0.0}}), + ], + ), + mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + ) as mock_fetch, + ): + report = build_report_data( + api, "octocat", **{**_DEFAULT_KWARGS, "include_consumers": True} + ) + actions = report["actions"] + self.assertEqual(actions["private_minutes"], 1000.0) + self.assertEqual(actions["public_minutes"], 500.0) + mock_fetch.assert_not_called() + + def test_actions_only_uses_fetch_repo_actions_table(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api( + actions_items=[ + { + "sku": "actions_linux", + "unitType": "minutes", + "grossQuantity": 1500.0, + "grossAmount": 0.0, + "discountAmount": 0.0, + "netAmount": 0.0, + } + ], + pages={"/user/repos": _TWO_REPOS}, + ) + with mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + return_value=(list(_DEFAULT_FAKE_ROWS), {}), + ) as mock_fetch: + report = build_report_data(api, "octocat", **_DEFAULT_KWARGS) + actions = report["actions"] + self.assertEqual(actions["private_minutes"], 1000.0) + self.assertEqual(actions["public_minutes"], 500.0) + mock_fetch.assert_called_once_with(api, _TWO_REPOS) + + def test_actions_disabled_skips_per_repo_fetch(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api(omit=("actions",)) + with mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + ) as mock_fetch: + report = build_report_data( + api, "octocat", **{**_DEFAULT_KWARGS, "include_actions": False} + ) + self.assertIsNone(report["actions"]) + mock_fetch.assert_not_called() + + def test_consumers_error_falls_back_to_fetch_repo_actions_table(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api( + actions_items=[ + { + "sku": "actions_linux", + "unitType": "minutes", + "grossQuantity": 1500.0, + "grossAmount": 0.0, + "discountAmount": 0.0, + "netAmount": 0.0, + } + ], + pages={"/user/repos": _TWO_REPOS}, + ) + with ( + mock.patch( + "github_usage.report_data.get_repo_consumers", + side_effect=RuntimeError("consumers API failure"), + ), + mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + return_value=(list(_DEFAULT_FAKE_ROWS), {}), + ) as mock_fetch, + ): + report = build_report_data( + api, "octocat", **{**_DEFAULT_KWARGS, "include_consumers": True} + ) + actions = report["actions"] + self.assertEqual(actions["private_minutes"], 1000.0) + self.assertEqual(actions["public_minutes"], 500.0) + mock_fetch.assert_called_once_with(api, _TWO_REPOS) + + def test_fallback_fetch_errors_propagated_into_report(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api(pages={"/user/repos": _TWO_REPOS}) + fallback_errors = {"octocat/private-repo": "billing fetch failed"} + with mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + return_value=([_PUBLIC_ROW], fallback_errors), + ): + report = build_report_data(api, "octocat", **_DEFAULT_KWARGS) + self.assertIn("octocat/private-repo", report["errors"]) + + def test_only_public_passes_through_to_visibility_split(self): + from github_usage.report_data import build_report_data + + api = _make_billing_api( + actions_items=[ + { + "sku": "actions_linux", + "unitType": "minutes", + "grossQuantity": 500.0, + "grossAmount": 0.0, + "discountAmount": 0.0, + "netAmount": 0.0, + } + ], + pages={"/user/repos": _TWO_REPOS}, + ) + public_row = {**_PUBLIC_ROW, "minutes": 500.0} + with mock.patch( + "github_usage.report_data.fetch_repo_actions_table", + return_value=([public_row], {}), + ) as mock_fetch: + report = build_report_data(api, "octocat", **{**_DEFAULT_KWARGS, "only_public": True}) + mock_fetch.assert_called_once_with(api, [_PUBLIC_REPO]) + self.assertTrue(report["actions"]["filtered"]) + self.assertEqual(report["actions"]["public_minutes"], 500.0) + self.assertEqual(report["actions"]["private_minutes"], 0.0) diff --git a/tests/test_workflow_minutes.py b/tests/test_workflow_minutes.py index ef5593e..594e477 100644 --- a/tests/test_workflow_minutes.py +++ b/tests/test_workflow_minutes.py @@ -447,6 +447,7 @@ def test_email_estimate_adds_workflow_headroom_when_consumers_enabled(self) -> N with_consumers = estimate_api_request_count( repo_count=5, + include_actions=False, include_consumers=True, include_artifact_storage=False, include_release_assets=False, @@ -454,6 +455,7 @@ def test_email_estimate_adds_workflow_headroom_when_consumers_enabled(self) -> N ) without = estimate_api_request_count( repo_count=5, + include_actions=False, include_consumers=False, include_artifact_storage=False, include_release_assets=False,