Implement Development Effort Calculator (WP-0010-T02)
src/target_revenue/effort_calculator.py implements Candidate A
(labor-cost-anchored, accepted T01): commit-timestamp session-gap
clustering for human interaction time, workplan/task-volume counts via
direct workplans/ directory parsing (no state-hub dependency, works
uniformly on any repo using this repo's own convention), file/line
counts with generated/vendored-path exclusion, and caller-supplied
token-cost pricing. estimate_target_basis() combines these and returns
a derivation dict (every input shown) plus a warnings list - never a
black-box dollar figure.
1-day manual-work floor, as requested: any raw commit-clustered
estimate below 1.0 day is floored and flagged with a warning that this
is very likely a measurement gap (commit-clustering is a floor
estimate by design) that should usually be compensated for by manual
override, not trusted at face value. A second, independent
sanity-check warning fires when finished-workplan/task volume is
substantial but the time estimate is still low, even above the floor -
demonstrated live against target-revenue's own history (7 finished
workplans, 57 tasks correctly flagged a 2.38-day estimate as
under-counted).
scripts/effort_calculator_cli.py: CLI wrapper printing JSON, following
the same offline-first, no-Phase-declaration pattern as
scripts/trf_onboard.py. tests/test_effort_calculator.py (15
deterministic tests, throwaway git repos/tmp_path fixtures) covers
commit clustering, workplan/task parsing, size-metric exclusion,
token-cost pricing, the floor-and-warning behavior, the sanity-check
warning, and an end-to-end smoke test. No new hard dependency.
2026-07-30 13:16:44 +02:00
|
|
|
"""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 ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
Apply Development Effort Calculator to real pilot candidates (WP-0010-T03)
Adds since/until date-range scoping to cluster_commit_hours() and
workplan_task_counts() (threaded through calculate_target_basis()),
needed whenever a candidate is one bounded workplan within a repo
whose overall history spans much more (net-kingdom, railiance-apps)
rather than the whole repo being the candidate (vergabe-teilnahme,
info-tech-canon).
Fixes a real bug found along the way: workplan_task_counts() only
scanned the top level of workplans/, missing net-kingdom's
workplans/archived/ convention entirely - silently reported zero
finished workplans for NK-WP-0002, which lives there. Fixed to scan
recursively; added a regression test.
Updates all three draft pilot-candidate manifests with calculator-
derived target_basis/initial_target values, replacing the hand-picked
placeholders:
net-kingdom-local-identity: 200,000 -> 10,000 EUR (floor + sanity warnings)
railiance-vergabe-teilnahme: 3,500,000 -> 648,800 EUR (no warnings)
info-tech-canon-service-surface: 2,500,000 -> 141,800 EUR (sanity warning)
history/260730-EffortCalculator-CandidateApplication.md records full
derivation, warnings, and the judgment calls made explicit rather than
silently picked (date-scoping windows; measuring vergabe-teilnahme's
own repo rather than railiance-apps' deployment-only wiring, with both
figures shown). Still draft/non-binding - WP-0008-T05 unaffected.
5 new tests (20 -> now covering since/until scoping and the
archived-subdirectory fix). Full suite: 84 passing offline.
2026-07-30 13:29:06 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
Implement Development Effort Calculator (WP-0010-T02)
src/target_revenue/effort_calculator.py implements Candidate A
(labor-cost-anchored, accepted T01): commit-timestamp session-gap
clustering for human interaction time, workplan/task-volume counts via
direct workplans/ directory parsing (no state-hub dependency, works
uniformly on any repo using this repo's own convention), file/line
counts with generated/vendored-path exclusion, and caller-supplied
token-cost pricing. estimate_target_basis() combines these and returns
a derivation dict (every input shown) plus a warnings list - never a
black-box dollar figure.
1-day manual-work floor, as requested: any raw commit-clustered
estimate below 1.0 day is floored and flagged with a warning that this
is very likely a measurement gap (commit-clustering is a floor
estimate by design) that should usually be compensated for by manual
override, not trusted at face value. A second, independent
sanity-check warning fires when finished-workplan/task volume is
substantial but the time estimate is still low, even above the floor -
demonstrated live against target-revenue's own history (7 finished
workplans, 57 tasks correctly flagged a 2.38-day estimate as
under-counted).
scripts/effort_calculator_cli.py: CLI wrapper printing JSON, following
the same offline-first, no-Phase-declaration pattern as
scripts/trf_onboard.py. tests/test_effort_calculator.py (15
deterministic tests, throwaway git repos/tmp_path fixtures) covers
commit clustering, workplan/task parsing, size-metric exclusion,
token-cost pricing, the floor-and-warning behavior, the sanity-check
warning, and an end-to-end smoke test. No new hard dependency.
2026-07-30 13:16:44 +02:00
|
|
|
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)
|
Apply Development Effort Calculator to real pilot candidates (WP-0010-T03)
Adds since/until date-range scoping to cluster_commit_hours() and
workplan_task_counts() (threaded through calculate_target_basis()),
needed whenever a candidate is one bounded workplan within a repo
whose overall history spans much more (net-kingdom, railiance-apps)
rather than the whole repo being the candidate (vergabe-teilnahme,
info-tech-canon).
Fixes a real bug found along the way: workplan_task_counts() only
scanned the top level of workplans/, missing net-kingdom's
workplans/archived/ convention entirely - silently reported zero
finished workplans for NK-WP-0002, which lives there. Fixed to scan
recursively; added a regression test.
Updates all three draft pilot-candidate manifests with calculator-
derived target_basis/initial_target values, replacing the hand-picked
placeholders:
net-kingdom-local-identity: 200,000 -> 10,000 EUR (floor + sanity warnings)
railiance-vergabe-teilnahme: 3,500,000 -> 648,800 EUR (no warnings)
info-tech-canon-service-surface: 2,500,000 -> 141,800 EUR (sanity warning)
history/260730-EffortCalculator-CandidateApplication.md records full
derivation, warnings, and the judgment calls made explicit rather than
silently picked (date-scoping windows; measuring vergabe-teilnahme's
own repo rather than railiance-apps' deployment-only wiring, with both
figures shown). Still draft/non-binding - WP-0008-T05 unaffected.
5 new tests (20 -> now covering since/until scoping and the
archived-subdirectory fix). Full suite: 84 passing offline.
2026-07-30 13:29:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 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
|