mirror of https://github.com/razor-ai/soup.git
test(embed): de-vacuous the model_id guards — assert the SPECIFIC message
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.
This commit is contained in:
parent
1eaeb8e46b
commit
6308038024
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue