diff --git a/leetcode_coach/leetcode_coach/phase.py b/leetcode_coach/leetcode_coach/phase.py index 3a196fb..627d590 100644 --- a/leetcode_coach/leetcode_coach/phase.py +++ b/leetcode_coach/leetcode_coach/phase.py @@ -102,9 +102,14 @@ def step2_attempts(events: list[dict[str, Any]]) -> int: A ``fail`` (the list read as code, or named an operation) never reaches the checker, so it has no ``req_check``; a ``partial`` does, and is - already counted there. CLI and app count one file alike. + already counted there. CLI and app count one file alike. A force + repeats the last try's grade but is not a try, so it is not counted. """ - fails = sum(1 for e in _step2_grades(events) if e.get("grade") == "fail") + fails = sum( + 1 + for e in _step2_grades(events) + if e.get("grade") == "fail" and e.get("forced") is not True + ) return req_state(events).tries + fails diff --git a/leetcode_coach/leetcode_coach/tests/test_app_events.py b/leetcode_coach/leetcode_coach/tests/test_app_events.py index 47b75fa..a547f3d 100644 --- a/leetcode_coach/leetcode_coach/tests/test_app_events.py +++ b/leetcode_coach/leetcode_coach/tests/test_app_events.py @@ -84,6 +84,16 @@ def test_step_two_attempts_count_incomplete_lists_and_fail_grades() -> None: assert phase.step2_attempts(events) == 3 +def test_a_force_is_not_a_step_two_try() -> None: + """A force repeats the last try's grade (`fail`); it is no try itself. + + Measured against the live relay 2026-10-23: three named-a-tool fails and + a force showed ``attempts: 4`` in the state. + """ + fails = [_grade(2, "fail"), _grade(2, "fail"), _grade(2, "fail")] + assert phase.step2_attempts([*fails, _grade(2, "fail", forced=True)]) == 3 + + def _gate(log: Path, answers: list[bool] | None = None) -> phase.PhaseGate: asked: list[str] = []