Skip to content

Commit 5ffb196

Browse files
committed
correct: Report mixed-case truncation
Replace the assert-complete deadline test with two counter-examples: five minority-case P that only the erasure reading corrects, and fifteen minority-case X with one mistyped that only case normalization corrects. Each case checks that its other reading alone finds nothing and that the shared schedule still returns the backup within the ten-second deadline. On the first case the normalized reading's exhaustive optional search takes minutes, so the test cannot require a complete search; it requires the truncation to be reported. Make _scheduled_candidates report that truncation. Combine both full passes' completeness instead of keeping only the later pass, and mark returned candidates search_complete=False when either pass truncated, so a copy from a completed earlier pass cannot hide the later timeout. refs #37 Claude-Session: https://claude.ai/code/session_013gZvwvuocM7a7Ut4kiBFHw
1 parent 9556136 commit 5ffb196

2 files changed

Lines changed: 68 additions & 13 deletions

File tree

‎src/codex32/_cli_input.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,16 +530,20 @@ def _scheduled_candidates(
530530
)
531531
if retry is None:
532532
return candidates, complete
533-
retry_candidates, complete, _deadline = search(
533+
retry_candidates, retry_complete, _deadline = search(
534534
retry, deadline=deadline, capture_layers=capture_layers, seed_candidates=(*seeded, *candidates)
535535
)
536+
complete = complete and retry_complete
536537
annotated = []
537538
for item in (*candidates, *retry_candidates):
538539
volume, bits = _capture_mass(capture_layers, item.capture_volume)
539540
annotated.append(replace(item, cumulative_capture_volume=volume, capture_space_bits=bits))
540541
unique: dict[str, CorrectionCandidate] = {}
541542
for item in _best(annotated, prefer_common=byte_length == "?", fingerprint_match=fingerprint_match):
542-
unique.setdefault(item.artifact.text.lower(), item)
543+
# A copy from a completed earlier pass must not hide later truncation.
544+
unique.setdefault(
545+
item.artifact.text.lower(), item if complete else replace(item, search_complete=False)
546+
)
543547
return tuple(unique.values()), complete
544548

545549

‎tests/test_mixed_case_deadline.py‎

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,82 @@
11
"""Deadline regression for mixed-case embedded recovery."""
22

3+
from dataclasses import replace
34
from time import monotonic
45

6+
import pytest
7+
58
from codex32 import Profile
6-
from codex32._cli_input import _case_interpretation, _scheduled_candidates
9+
from codex32._cli_input import _case_interpretation, _correction_candidates, _scheduled_candidates
710

11+
SOURCE = "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"
812

9-
def test_embedded_mixed_case_completes_both_interpretations_within_deadline() -> None:
10-
source = "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"
11-
damaged = "ms10testsxPxxxxxPxxxxxPxxxxxPxxxxxP4nzvca9cmczlw"
13+
14+
@pytest.mark.parametrize(
15+
("damaged", "erasure_fixes", "normalized_fixes"),
16+
[
17+
# Five minority-case P: five erasures are correctable, five substitutions are not.
18+
("ms10testsxPxxxxxPxxxxxPxxxxxPxxxxxP4nzvca9cmczlw", True, False),
19+
# Fifteen minority-case X, one mistyped: fifteen erasures are not correctable,
20+
# one substitution after case normalization is.
21+
("ms10testsXXXXXXXPXXXXXXXxxxxxxxxxxx4nzvca9cmczlw", False, True),
22+
],
23+
)
24+
def test_embedded_mixed_case_recovers_either_sole_interpretation_within_deadline(
25+
damaged: str, erasure_fixes: bool, normalized_fixes: bool
26+
) -> None:
1227
interpretation = _case_interpretation(damaged, "", (Profile.MS,), None)
1328

1429
assert interpretation is not None
1530
direct, normalized, erased, _prefix = interpretation
1631
assert direct is None
17-
separator = normalized.rfind("1")
18-
deadline = monotonic() + 10
32+
immutable = normalized[: normalized.rfind("1") + 1]
33+
for value, fixes in ((erased, erasure_fixes), (normalized, normalized_fixes)):
34+
alone, alone_complete, _deadline = _correction_candidates(
35+
value, Profile.MS, None, immutable, deadline=monotonic() + 10, required_only=True
36+
)
37+
assert alone_complete
38+
assert [candidate.artifact.text for candidate in alone] == ([SOURCE] if fixes else [])
1939

2040
candidates, complete = _scheduled_candidates(
2141
normalized,
2242
erased,
2343
Profile.MS,
2444
None,
25-
normalized[: separator + 1],
26-
deadline=deadline,
45+
immutable,
46+
deadline=monotonic() + 10,
2747
)
2848

29-
assert complete
30-
assert len(candidates) == 1
31-
assert candidates[0].artifact.text == source
49+
# The exhaustive optional search may truncate at the deadline; the
50+
# candidate must still be found and must report that truncation.
51+
assert [candidate.artifact.text for candidate in candidates] == [SOURCE]
52+
assert candidates[0].search_complete is complete
53+
54+
55+
def test_scheduled_truncation_survives_a_complete_later_pass(monkeypatch: pytest.MonkeyPatch) -> None:
56+
damaged = "ms10testsxPxxxxxPxxxxxPxxxxxPxxxxxP4nzvca9cmczlw"
57+
interpretation = _case_interpretation(damaged, "", (Profile.MS,), None)
58+
assert interpretation is not None
59+
_direct, normalized, erased, _prefix = interpretation
60+
immutable = normalized[: normalized.rfind("1") + 1]
61+
found, _complete, _deadline = _correction_candidates(
62+
erased, Profile.MS, None, immutable, deadline=monotonic() + 10, required_only=True
63+
)
64+
full_searches: list[str] = []
65+
66+
def truncated_first(value: str, *_args: object, **kwargs: object) -> tuple[object, bool, float]:
67+
if kwargs.get("required_only"):
68+
return (), True, 0.0
69+
full_searches.append(value)
70+
if len(full_searches) == 1:
71+
return (replace(found[0], search_complete=False),), False, 0.0
72+
return (), True, 0.0
73+
74+
monkeypatch.setattr("codex32._cli_input._correction_candidates", truncated_first)
75+
76+
candidates, complete = _scheduled_candidates(normalized, erased, Profile.MS, None, immutable)
77+
78+
assert full_searches == [erased, normalized]
79+
assert not complete
80+
assert [(candidate.artifact.text, candidate.search_complete) for candidate in candidates] == [
81+
(SOURCE, False)
82+
]

0 commit comments

Comments
 (0)