"""Tests for target_revenue.effort_calculator (WP-0010-T02). Pure/offline: builds throwaway git repos and directory fixtures under tmp_path rather than depending on any real repo's current state, so these tests stay deterministic regardless of what target-revenue's own history looks like later. """ from __future__ import annotations import subprocess from pathlib import Path import pytest from target_revenue import effort_calculator as ec def _git(repo: Path, *args: str, env: dict | None = None) -> None: subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True, env=env) def _commit(repo: Path, message: str, epoch_seconds: int) -> None: (repo / "file.txt").write_text(message) _git(repo, "add", "file.txt") date = f"{epoch_seconds} +0000" env = { "GIT_AUTHOR_DATE": date, "GIT_COMMITTER_DATE": date, "GIT_AUTHOR_NAME": "test", "GIT_AUTHOR_EMAIL": "test@example.com", "GIT_COMMITTER_NAME": "test", "GIT_COMMITTER_EMAIL": "test@example.com", } import os full_env = {**os.environ, **env} _git(repo, "commit", "-q", "-m", message, env=full_env) @pytest.fixture() def git_repo(tmp_path): repo = tmp_path / "repo" repo.mkdir() _git(repo, "init", "-q", "-b", "main") return repo # --- cluster_commit_hours ---------------------------------------------- def test_cluster_commit_hours_empty_repo_no_commits(git_repo): result = ec.cluster_commit_hours(git_repo) assert result == ec.CommitTimeEstimate(raw_hours=0.0, session_count=0, commit_count=0) def test_cluster_commit_hours_single_commit_uses_floor(git_repo): _commit(git_repo, "init", 1_000_000_000) result = ec.cluster_commit_hours(git_repo) assert result.commit_count == 1 assert result.session_count == 1 assert result.raw_hours == pytest.approx(ec.SINGLE_COMMIT_SESSION_MINUTES / 60) def test_cluster_commit_hours_one_session_spans_actual_time(git_repo): base = 1_000_000_000 _commit(git_repo, "c1", base) _commit(git_repo, "c2", base + 3600) # 1 hour later, same session result = ec.cluster_commit_hours(git_repo, session_gap_hours=2.0) assert result.session_count == 1 assert result.commit_count == 2 assert result.raw_hours == pytest.approx(1.0) def test_cluster_commit_hours_gap_splits_into_two_sessions(git_repo): base = 1_000_000_000 _commit(git_repo, "c1", base) _commit(git_repo, "c2", base + 3 * 3600) # 3 hours later, gap > 2h default result = ec.cluster_commit_hours(git_repo, session_gap_hours=2.0) assert result.session_count == 2 assert result.commit_count == 2 # Both are single-commit sessions -> two floor contributions. assert result.raw_hours == pytest.approx(2 * ec.SINGLE_COMMIT_SESSION_MINUTES / 60) # --- workplan_task_counts ------------------------------------------------ def test_workplan_task_counts_scans_archived_subdirectory(tmp_path): archived = tmp_path / "workplans" / "archived" archived.mkdir(parents=True) (archived / "old-finished.md").write_text( """--- id: OLD-0001 status: finished --- ```task id: OLD-0001-T01 status: done ``` """ ) result = ec.workplan_task_counts(tmp_path) assert result.finished_workplans == 1 assert result.total_tasks == 1 assert result.finished_tasks == 1 def test_workplan_task_counts_no_workplans_dir(tmp_path): result = ec.workplan_task_counts(tmp_path) assert result == ec.WorkplanTaskCounts(0, 0, 0) def test_workplan_task_counts_counts_finished_workplans_and_tasks(tmp_path): workplans = tmp_path / "workplans" workplans.mkdir() (workplans / "WP-0001.md").write_text( """--- id: WP-0001 status: finished --- ```task id: WP-0001-T01 status: done ``` ```task id: WP-0001-T02 status: todo ``` """ ) (workplans / "WP-0002.md").write_text( """--- id: WP-0002 status: active --- ```task id: WP-0002-T01 status: done ``` """ ) result = ec.workplan_task_counts(tmp_path) assert result.finished_workplans == 1 assert result.total_tasks == 3 assert result.finished_tasks == 2 # --- repo_size_metrics ---------------------------------------------------- def test_repo_size_metrics_counts_files_and_lines(tmp_path): (tmp_path / "a.py").write_text("line1\nline2\nline3\n") (tmp_path / "b.py").write_text("line1\n") result = ec.repo_size_metrics(tmp_path) assert result.file_count == 2 assert result.line_count == 4 def test_repo_size_metrics_excludes_vendored_paths(tmp_path): (tmp_path / "a.py").write_text("line1\n") vendored = tmp_path / "node_modules" / "pkg" vendored.mkdir(parents=True) (vendored / "big.js").write_text("x\n" * 1000) result = ec.repo_size_metrics(tmp_path) assert result.file_count == 1 assert result.line_count == 1 # --- token_cost_usd -------------------------------------------------------- def test_token_cost_usd_default_pricing(): cost = ec.token_cost_usd(1_000_000, 1_000_000) expected = 1_000_000 * 3e-6 + 1_000_000 * 15e-6 assert cost == pytest.approx(expected) def test_token_cost_usd_unknown_model_falls_back_to_default(): cost_unknown = ec.token_cost_usd(1000, 1000, model="some-unlisted-model") cost_default = ec.token_cost_usd(1000, 1000, model="default") assert cost_unknown == cost_default # --- estimate_target_basis: the 1-day floor and warning ------------------- def test_low_raw_estimate_triggers_floor_and_warning(): commit_time = ec.CommitTimeEstimate(raw_hours=0.5, session_count=1, commit_count=1) result = ec.estimate_target_basis( commit_time=commit_time, workplan_tasks=ec.WorkplanTaskCounts(0, 0, 0), repo_size=ec.RepoSizeMetrics(1, 10), token_cost=ec.TokenCost(0, 0, 0.0), daily_rate=1000.0, ) assert result.estimated_effort_days == ec.MANUAL_EFFORT_FLOOR_DAYS assert any("floor" in w.lower() for w in result.warnings) assert any("manual override" in w.lower() for w in result.warnings) assert result.derivation["raw_commit_clustered_days"] < ec.MANUAL_EFFORT_FLOOR_DAYS def test_sufficient_raw_estimate_no_floor_warning(): commit_time = ec.CommitTimeEstimate( raw_hours=ec.DEFAULT_HOURS_PER_DAY * 5, session_count=5, commit_count=20 ) result = ec.estimate_target_basis( commit_time=commit_time, workplan_tasks=ec.WorkplanTaskCounts(0, 0, 0), repo_size=ec.RepoSizeMetrics(10, 500), token_cost=ec.TokenCost(0, 0, 0.0), daily_rate=1000.0, ) assert result.estimated_effort_days == pytest.approx(5.0) assert not any("floor" in w.lower() for w in result.warnings) def test_high_workplan_volume_with_low_time_triggers_sanity_warning(): commit_time = ec.CommitTimeEstimate(raw_hours=4.0, session_count=1, commit_count=2) result = ec.estimate_target_basis( commit_time=commit_time, workplan_tasks=ec.WorkplanTaskCounts(finished_workplans=10, total_tasks=50, finished_tasks=40), repo_size=ec.RepoSizeMetrics(200, 20000), token_cost=ec.TokenCost(0, 0, 0.0), daily_rate=1000.0, ) assert any("under-counted" in w.lower() for w in result.warnings) def test_derivation_shows_its_work(): commit_time = ec.CommitTimeEstimate(raw_hours=16.0, session_count=2, commit_count=10) result = ec.estimate_target_basis( commit_time=commit_time, workplan_tasks=ec.WorkplanTaskCounts(2, 5, 3), repo_size=ec.RepoSizeMetrics(50, 3000), token_cost=ec.TokenCost(tokens_in=100, tokens_out=200, usd=1.23), daily_rate=800.0, ) assert result.derivation["commits"] == 10 assert result.derivation["sessions"] == 2 assert result.derivation["finished_workplans"] == 2 assert result.derivation["file_count"] == 50 assert result.derivation["line_count"] == 3000 assert result.derivation["tokens_in"] == 100 assert result.approved_direct_costs == 1.23 assert result.daily_rate == 800.0 # --- calculate_target_basis: end-to-end ----------------------------------- def test_calculate_target_basis_end_to_end(git_repo): base = 1_000_000_000 _commit(git_repo, "c1", base) _commit(git_repo, "c2", base + ec.DEFAULT_HOURS_PER_DAY * 3 * 3600) result = ec.calculate_target_basis( git_repo, daily_rate=1000.0, tokens_in=1000, tokens_out=1000 ) assert result.daily_rate == 1000.0 assert result.approved_direct_costs == pytest.approx( ec.token_cost_usd(1000, 1000), abs=0.01 ) assert isinstance(result.estimated_effort_days, float) # --- since/until date-range scoping --------------------------------------- def test_cluster_commit_hours_since_until_excludes_outside_window(git_repo): _commit(git_repo, "outside-early", 1_577_836_800) # 2020-01-01 _commit(git_repo, "inside", 1_614_556_800) # 2021-03-01 _commit(git_repo, "outside-late", 1_672_531_200) # 2023-01-01 result = ec.cluster_commit_hours(git_repo, since="2021-01-01", until="2021-06-01") assert result.commit_count == 1 def test_workplan_task_counts_since_until_filters_by_updated_date(tmp_path): workplans = tmp_path / "workplans" workplans.mkdir() (workplans / "old.md").write_text( """--- id: OLD-0001 status: finished updated: "2020-01-01" --- ```task id: OLD-0001-T01 status: done ``` """ ) (workplans / "recent.md").write_text( """--- id: NEW-0001 status: finished updated: "2026-03-03" --- ```task id: NEW-0001-T01 status: done ``` """ ) result = ec.workplan_task_counts(tmp_path, since="2026-01-01", until="2026-06-01") assert result.finished_workplans == 1 assert result.total_tasks == 1 def test_workplan_task_counts_no_date_field_always_counted(tmp_path): workplans = tmp_path / "workplans" workplans.mkdir() (workplans / "undated.md").write_text( """--- id: UND-0001 status: finished --- ```task id: UND-0001-T01 status: done ``` """ ) result = ec.workplan_task_counts(tmp_path, since="2026-01-01", until="2026-06-01") assert result.finished_workplans == 1 assert result.total_tasks == 1 def test_calculate_target_basis_threads_since_until(git_repo): base = 1_577_836_800 # 2020-01-01 _commit(git_repo, "outside", base) _commit(git_repo, "inside", base + 86400 * 400) # ~2021-02-04 result = ec.calculate_target_basis( git_repo, daily_rate=1000.0, since="2021-01-01", until="2021-06-01" ) assert result.derivation["commits"] == 1