Skip to content

Commit 570518f

Browse files
committed
test(#88): attribution must not break the read when classify_build raises — failing-first
recipe_is_structurally_nondeterministic reads the build's serialized ops via classify_build (Path.glob + read_text). On a missing/unreadable build dir that raises, and the call sits inside the cached_result_expr self-heal block OUTSIDE its try/except, so the unguarded raise propagates out of the read it was only meant to annotate. The predicate's own docstring promises best-effort 'returns False', so a fault must degrade to the execution (#83) label, not kill the read. Pre-warms the plan memo so cache_worthy's classify_build call is not re-invoked, isolating the predicate's line-360 call as the one statement under test.
1 parent b710ae7 commit 570518f

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

tests/test_result_cache.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,58 @@ def test_self_heal_warns_on_unfaithful_recompute(project, orders_parquet, monkey
704704
assert any("UNFAITHFUL" in m and h in m and "execution (#83)" in m for m in msgs), msgs
705705

706706

707+
def test_self_heal_warning_degrades_to_execution_when_classify_build_raises(
708+
project, orders_parquet, monkeypatch, caplog
709+
):
710+
# #88 part 2, best-effort contract: the structural-vs-execution attribution must
711+
# never break the read. recipe_is_structurally_nondeterministic reads the build's
712+
# serialized ops via classify_build (Path.glob + read_text); on a missing or
713+
# unreadable build dir that raises, and the call sits inside the self-heal block
714+
# OUTSIDE its surrounding try/except, so an unguarded raise propagates out of
715+
# cached_result_expr and kills the read it was only meant to annotate. The
716+
# predicate must swallow that and degrade to False (not-structural), so the drift
717+
# is still labeled execution (#83) and the read still succeeds.
718+
import logging
719+
720+
from tallyman_xorq.result_cache import baked_snapshot_path, cached_result_expr
721+
722+
monkeypatch.setenv("TALLYMAN_SOURCE_IDENTITY", "off")
723+
monkeypatch.setenv("TALLYMAN_PROJECT", project)
724+
catalog_create("agg", _agg_code(project)) # expensive → baked snapshot
725+
h = _hash_of(project)
726+
727+
# Warm the plan memo with a working classify_build FIRST: _resolve_result_plan
728+
# calls cache_worthy → classify_build too, so memoising the plan here means the
729+
# self-heal read below skips that call and the only classify_build left on the
730+
# path is the predicate's at line 360 — the one statement under test.
731+
cached_result_expr.cache_clear()
732+
cached_result_expr(project, h).execute()
733+
734+
# Drift the source and evict the snapshot so the next read self-heals to bytes
735+
# that differ from the recorded digest, firing the faithfulness warning.
736+
_overwrite_orders(project, seed=1)
737+
p = baked_snapshot_path(project, h)
738+
assert p is not None and p.exists()
739+
p.unlink()
740+
741+
# Now make the predicate's classify_build raise. The plan stays memoised (no
742+
# cache_clear), so cache_worthy is not re-invoked; only recipe_is_structurally_
743+
# nondeterministic hits this on the self-heal warning path.
744+
def _raise(*_a, **_k):
745+
raise OSError("build_dir inaccessible")
746+
747+
monkeypatch.setattr("tallyman_xorq.result_cache.classify_build", _raise)
748+
749+
caplog.clear()
750+
with caplog.at_level(logging.WARNING, logger="tallyman.perf"):
751+
df = cached_result_expr(project, h).execute() # must NOT raise despite the fault
752+
assert len(df) > 0
753+
msgs = [r.getMessage() for r in caplog.records if r.name == "tallyman.perf"]
754+
# Degraded to not-structural → execution axis, never a false structural claim.
755+
assert any("UNFAITHFUL" in m and h in m and "execution (#83)" in m for m in msgs), msgs
756+
assert not any("structural (#88)" in m for m in msgs), msgs
757+
758+
707759
def test_cached_result_expr_self_heals_after_warm_then_evict(project, orders_parquet, monkeypatch):
708760
"""An expensive entry's baked snapshot evicted *after* a warm read must still
709761
self-heal on the next read, not dangle on a stale ``deferred_read_parquet``.

0 commit comments

Comments
 (0)