test(failcount): add 13 unit tests + 6 coverage tests; 100% coverage achieved
This commit is contained in:
@@ -32,3 +32,177 @@ def test_initial_state_zero(fresh_state: FailcountState) -> None:
|
||||
assert fresh_state.red_phase_failures == 0
|
||||
assert fresh_state.green_phase_failures == 0
|
||||
assert fresh_state.no_progress_started_at is None
|
||||
|
||||
|
||||
def test_red_phase_failure_increments(fresh_state: FailcountState) -> None:
|
||||
after_one = record_red_failure(fresh_state)
|
||||
assert after_one.red_phase_failures == 1
|
||||
after_two = record_red_failure(after_one)
|
||||
assert after_two.red_phase_failures == 2
|
||||
|
||||
|
||||
def test_green_success_resets_red_counter(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
after_two_red = record_red_failure(record_red_failure(fresh_state))
|
||||
assert after_two_red.red_phase_failures == 2
|
||||
now = datetime.now(timezone.utc)
|
||||
after_green = record_green_success(after_two_red, now)
|
||||
assert after_green.red_phase_failures == 0
|
||||
|
||||
|
||||
def test_green_phase_failure_increments(fresh_state: FailcountState) -> None:
|
||||
after_one = record_green_failure(fresh_state)
|
||||
assert after_one.green_phase_failures == 1
|
||||
after_two = record_green_failure(after_one)
|
||||
assert after_two.green_phase_failures == 2
|
||||
|
||||
|
||||
def test_no_progress_advances(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
started = datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||
after_record = record_commit(fresh_state, started)
|
||||
later = started + timedelta(minutes=31)
|
||||
assert should_give_up(after_record, default_config, later) is True
|
||||
|
||||
|
||||
def test_no_progress_resets_on_commit(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
started = datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||
state_with_timer = record_commit(fresh_state, started)
|
||||
later = started + timedelta(minutes=25)
|
||||
state_with_reset = record_commit(state_with_timer, later)
|
||||
final = later + timedelta(minutes=25)
|
||||
assert should_give_up(state_with_reset, default_config, final) is False
|
||||
|
||||
|
||||
def test_no_progress_resets_on_green(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
started = datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||
state_with_timer = record_commit(fresh_state, started)
|
||||
later = started + timedelta(minutes=25)
|
||||
state_with_reset = record_green_success(state_with_timer, later)
|
||||
final = later + timedelta(minutes=25)
|
||||
assert should_give_up(state_with_reset, default_config, final) is False
|
||||
|
||||
|
||||
def test_threshold_fires_at_three(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
s = record_red_failure(record_red_failure(record_red_failure(fresh_state)))
|
||||
now = datetime.now(timezone.utc)
|
||||
assert should_give_up(s, default_config, now) is True
|
||||
|
||||
|
||||
def test_threshold_does_not_fire_at_two(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
s = record_red_failure(record_red_failure(fresh_state))
|
||||
now = datetime.now(timezone.utc)
|
||||
assert should_give_up(s, default_config, now) is False
|
||||
|
||||
|
||||
def test_multi_signal_independence(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
s = record_red_failure(record_red_failure(fresh_state))
|
||||
assert s.red_phase_failures == 2
|
||||
assert s.green_phase_failures == 0
|
||||
s2 = record_green_failure(s)
|
||||
assert s2.red_phase_failures == 2
|
||||
assert s2.green_phase_failures == 1
|
||||
|
||||
|
||||
def test_any_signal_triggers(
|
||||
fresh_state: FailcountState, default_config: FailcountConfig
|
||||
) -> None:
|
||||
s = record_green_failure(record_green_failure(record_green_failure(fresh_state)))
|
||||
assert s.red_phase_failures == 0
|
||||
now = datetime.now(timezone.utc)
|
||||
assert should_give_up(s, default_config, now) is True
|
||||
|
||||
|
||||
def test_state_persistence_round_trip() -> None:
|
||||
original = FailcountState(
|
||||
red_phase_failures=2,
|
||||
green_phase_failures=1,
|
||||
no_progress_started_at=datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
d = to_dict(original)
|
||||
restored = from_dict(d)
|
||||
assert restored.red_phase_failures == 2
|
||||
assert restored.green_phase_failures == 1
|
||||
assert restored.no_progress_started_at == datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_configurable_thresholds() -> None:
|
||||
config = FailcountConfig(
|
||||
red_phase_threshold=5,
|
||||
green_phase_threshold=2,
|
||||
no_progress_minutes=10,
|
||||
)
|
||||
state = FailcountState(red_phase_failures=4)
|
||||
now = datetime.now(timezone.utc)
|
||||
assert should_give_up(state, config, now) is False
|
||||
state_g = FailcountState(green_phase_failures=2)
|
||||
assert should_give_up(state_g, config, now) is True
|
||||
|
||||
|
||||
def test_load_config_reads_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parent.parent))
|
||||
import importlib
|
||||
import scripts.tier2.failcount as fc_module
|
||||
monkeypatch.setattr(fc_module, "load_config", lambda: fc_module._load_config_from_toml(tmp_path / "fake.toml"))
|
||||
cfg = fc_module.load_config()
|
||||
assert cfg.red_phase_threshold == 3
|
||||
assert cfg.green_phase_threshold == 3
|
||||
assert cfg.no_progress_minutes == 30
|
||||
|
||||
|
||||
def test_load_config_overrides_from_toml(tmp_path: Path) -> None:
|
||||
toml_path = tmp_path / "custom.toml"
|
||||
toml_path.write_text("red_phase_threshold = 7\ngreen_phase_threshold = 4\nno_progress_minutes = 60\n", encoding="utf-8")
|
||||
from scripts.tier2.failcount import _load_config_from_toml
|
||||
cfg = _load_config_from_toml(toml_path)
|
||||
assert cfg.red_phase_threshold == 7
|
||||
assert cfg.green_phase_threshold == 4
|
||||
assert cfg.no_progress_minutes == 60
|
||||
|
||||
|
||||
def test_save_and_load_state_round_trip(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TIER2_STATE_DIR", str(tmp_path))
|
||||
original = FailcountState(
|
||||
red_phase_failures=2,
|
||||
green_phase_failures=1,
|
||||
no_progress_started_at=datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
save_state("my_track", original)
|
||||
loaded = load_state("my_track")
|
||||
assert loaded.red_phase_failures == 2
|
||||
assert loaded.green_phase_failures == 1
|
||||
assert loaded.no_progress_started_at == datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_load_state_missing_returns_fresh(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TIER2_STATE_DIR", str(tmp_path))
|
||||
loaded = load_state("nonexistent_track")
|
||||
assert loaded.red_phase_failures == 0
|
||||
assert loaded.green_phase_failures == 0
|
||||
assert loaded.no_progress_started_at is None
|
||||
|
||||
|
||||
def test_save_state_creates_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TIER2_STATE_DIR", str(tmp_path))
|
||||
save_state("track", FailcountState(red_phase_failures=5))
|
||||
assert (tmp_path / "track" / "state.json").exists()
|
||||
|
||||
|
||||
def test_load_config_integration_reads_real_toml() -> None:
|
||||
from scripts.tier2.failcount import load_config
|
||||
cfg = load_config()
|
||||
assert cfg.red_phase_threshold == 3
|
||||
assert cfg.green_phase_threshold == 3
|
||||
assert cfg.no_progress_minutes == 30
|
||||
|
||||
Reference in New Issue
Block a user