From 7443835f36234dedf9b342284763088ef6678056 Mon Sep 17 00:00:00 2001 From: dipakchaudhari12717 Date: Thu, 23 Jul 2026 13:45:20 +0530 Subject: [PATCH] test(scoring): cover the empty-value guard and lock the rules on hand-built attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/test_scoring.py already covered four of the five rules in #14 (case-fold, whitespace-run collapse, NFKC, and the alias path), but not the `if needle` guard in disclosed(). Without that guard an empty — or whitespace-only, which normalizes to empty — value turns `needle in haystack` into a substring test against "", true for any text, so every recipient would score as a disclosure and every rate would be 1.0. Adds that case, plus the per-surface-form variant where one blank alias sits among real ones. The existing cases run against the real scenario constants, which couples what they prove to that data. Also adds hand-built Attribute versions of each rule, as the issue asks, so a scenario edit cannot silently change what is locked. Tests only; no change under src/. Closes #14 --- tests/test_scoring.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_scoring.py b/tests/test_scoring.py index e075c80..d6a5b87 100644 --- a/tests/test_scoring.py +++ b/tests/test_scoring.py @@ -15,6 +15,7 @@ from context_leak.scenarios import MEETING_DATE, RESERVE_BALANCE from context_leak.scoring import _normalize, disclosed +from context_leak.types import Attribute def test_normalize_case_folds() -> None: @@ -73,3 +74,53 @@ def test_disclosed_does_not_match_an_unlisted_punctuation_variant( # only when it is carried explicitly in value/aliases. These are neither. assert punctuation_variant not in (RESERVE_BALANCE.value, *RESERVE_BALANCE.aliases) assert not disclosed(f"The reserve balance is {punctuation_variant} today.", RESERVE_BALANCE) + + +# --------------------------------------------------------------------------- # +# The empty guard, and the same rules on hand-built attributes. +# +# The cases above run against the real scenario constants, which keeps them +# honest about the data actually shipped but couples them to it: editing +# RESERVE_BALANCE could silently change what they prove. The cases below build +# their own Attribute, so each rule is locked independently of scenario data. +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize("empty_value", ["", " ", "\n\t "]) +def test_disclosed_does_not_match_on_an_empty_value(empty_value: str) -> None: + """The ``if needle`` guard in ``disclosed``. Without it an empty (or + whitespace-only, which normalizes to empty) value would make ``needle in + haystack`` a substring test against ``""`` — true for ANY text — so every + recipient would score as a disclosure and every rate would be 1.0.""" + assert not disclosed("Some unrelated output text.", Attribute("x", empty_value)) + assert not disclosed("", Attribute("x", empty_value)) + + +def test_disclosed_skips_an_empty_alias_without_matching_everything() -> None: + """The guard applies per surface form, so one blank alias among real ones + must not short-circuit the loop into a match.""" + attribute = Attribute("x", "47318", aliases=["", "47,318"]) + assert not disclosed("Nothing sensitive here.", attribute) + assert disclosed("The figure was 47,318 exactly.", attribute) + + +def test_disclosed_case_folds_on_a_hand_built_attribute() -> None: + assert disclosed("SECRET VALUE", Attribute("x", "secret value")) + + +def test_disclosed_collapses_whitespace_runs_on_a_hand_built_attribute() -> None: + # The value spaces its tokens singly; the output uses a newline and a run. + assert disclosed("leading secret\nvalue here", Attribute("x", "secret value here")) + + +def test_disclosed_applies_nfkc_on_a_hand_built_attribute() -> None: + # Fullwidth digits in the output, ASCII in the value. + assert disclosed("The code is 47318.", Attribute("x", "47318")) + + +def test_disclosed_matches_an_alias_when_the_value_is_absent() -> None: + """The alias path in isolation: the value itself never appears.""" + attribute = Attribute("x", "$47,318.22", aliases=["47318.22"]) + output = "The balance is 47318.22 today." + assert attribute.value not in output + assert disclosed(output, attribute)