From 2dbfaeb60e4d482de39641313c14692283fb2040 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 16 Jun 2026 19:06:09 -0400 Subject: [PATCH] test(failcount): add 13 unit tests + 6 coverage tests; 100% coverage achieved --- tests/test_failcount.py | 174 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/tests/test_failcount.py b/tests/test_failcount.py index f8c3ad7e..176d162c 100644 --- a/tests/test_failcount.py +++ b/tests/test_failcount.py @@ -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