From 6308038024a28734b76c77aa9ddc33ad37b68299 Mon Sep 17 00:00:00 2001 From: Alpamys Date: Thu, 16 Jul 2026 21:50:45 +0500 Subject: [PATCH] =?UTF-8?q?test(embed):=20de-vacuous=20the=20model=5Fid=20?= =?UTF-8?q?guards=20=E2=80=94=20assert=20the=20SPECIFIC=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ECC tdd pass, run as an 18-mutant sweep across the areas the earlier per-module harnesses had not covered (topics internals, embed validation, semdedup guards). 16 mutants were caught; the sweep found one real vacuity, which is the 7th of this release. test_bad_model_id_rejected asserted only `pytest.raises((ValueError, TypeError))` over ["", " ", None, 123, True]. That is too broad to mean anything here: with the empty-check DELETED, "" falls straight through to the fallback refusal ("cannot verify pooling") and still raises ValueError, so the test passed for a completely different reason than its name claims. Proven by mutation: deleting the empty guard left the suite green. The length cap had no test at all; the null-byte check had none either. Replaced with one test per guard, each asserting the message that guard actually emits (non-empty / null byte / too long / must be str), plus a monkeypatched _fetch_pooling_config that fails the test if the guard lets execution reach a network fetch. Re-ran the sweep: all three guards now CAUGHT where two previously survived. Also added: model_id is stripped before the allowlist lookup, so surrounding whitespace cannot defeat it. Verified NOT vacuous (mutation-checked, no change needed): the ctfidf top_n slice, resolve_k's cap/clamp/n<4 rules, kmeans' iteration loop, build_topic_report's coverage denominator + gap warning, embed's row cap / non-str / bare-string / truncation / batch bounds / L2 zero-guard / allowlist lowercasing, semdedup's row cap / 2-D check / nearest-kept-row provenance. 268 tests green (1 POSIX skip), ruff clean. --- tests/test_v07136.py | 50 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/tests/test_v07136.py b/tests/test_v07136.py index b5af13b..600c7da 100644 --- a/tests/test_v07136.py +++ b/tests/test_v07136.py @@ -112,13 +112,57 @@ class TestResolvePooling: embed.resolve_pooling("org/other-encoder") assert "max" in str(exc.value) - @pytest.mark.parametrize("bad", ["", " ", None, 123, True]) - def test_bad_model_id_rejected(self, bad): + @pytest.mark.parametrize("bad", [None, 123, True, 1.5, b"bytes"]) + def test_non_string_model_id_rejected(self, bad): from soup_cli.utils.embed import resolve_pooling - with pytest.raises((ValueError, TypeError)): + with pytest.raises(TypeError, match="must be str"): resolve_pooling(bad) + # Each guard below asserts its OWN message. A bare + # `pytest.raises((ValueError, TypeError))` was vacuous here: with the + # empty-check deleted, "" falls through to the FALLBACK refusal + # ("cannot verify pooling") and still raises ValueError, so the broad + # form could not tell the two apart — proven by mutation. + @pytest.mark.parametrize("bad", ["", " ", "\t\n"]) + def test_empty_model_id_names_the_empty_guard(self, bad, monkeypatch): + from soup_cli.utils import embed + + monkeypatch.setattr( + embed, "_fetch_pooling_config", + lambda mid: pytest.fail("must reject before any fetch"), + ) + with pytest.raises(ValueError, match="non-empty"): + embed.resolve_pooling(bad) + + def test_null_byte_model_id_names_the_null_guard(self, monkeypatch): + from soup_cli.utils import embed + + monkeypatch.setattr( + embed, "_fetch_pooling_config", + lambda mid: pytest.fail("must reject before any fetch"), + ) + with pytest.raises(ValueError, match="null byte"): + embed.resolve_pooling("org/model\x00evil") + + def test_overlong_model_id_names_the_length_guard(self, monkeypatch): + from soup_cli.utils import embed + + monkeypatch.setattr( + embed, "_fetch_pooling_config", + lambda mid: pytest.fail("must reject before any fetch"), + ) + with pytest.raises(ValueError, match="too long"): + embed.resolve_pooling("o/" + "x" * (embed._MAX_MODEL_ID_CHARS + 1)) + + def test_model_id_is_stripped_not_just_validated(self): + from soup_cli.utils.embed import resolve_pooling + + # surrounding whitespace must not defeat the allowlist + assert resolve_pooling( + " sentence-transformers/all-MiniLM-L6-v2 " + ) == "mean" + class TestEmbedTexts: def test_rejects_over_cap(self):