Three independent audit agents read each generated suite plus the source modules they target. Their findings converged on the same anti-patterns from different files, which is stronger than any single audit would be. This document synthesizes their conclusions.
Coverage % is misleading us. All three suites land between 97.7 % and 100 %
line coverage on src/itsdangerous/, yet none of them is meaningfully more
rigorous than the original 37 hand-written tests — and all three are
measurably more fragile. The original suite is dense and behavioral; the
generated suites are large, explicit, and pin implementation details the
maintainer never promised.
| Policy | Tests | LOC | Line % | Branch % | Pass | Fail | Mock LOC | Behavioral grade |
|---|---|---|---|---|---|---|---|---|
| original | 37 | 481 | 97.65 % | 94.90 % | 297 | 0 | 0 | reference |
| oneshot | 159 | 1091 | 97.71 % | 95.92 % | 157 | 2 | 0 | weakest |
| iter2 | 176 | 1185 | 100.00 % | 100.00 % | 176 | 0 | 0 | mid |
| iter20 | 163 | 1121 | 100.00 % | 100.00 % | 163 | 0 | 0 | mid (≈ iter2) |
Note: iter20 stopped at iter_2 because coverage targets were met. The iter20 budget was wasted — for a 97 % baseline, 2 iterations is enough to hit 100 %, and "the budget went unused" is itself a finding.
The single most common fragility across all three suites. Every policy pins prose:
| Policy | Example |
|---|---|
| oneshot | pytest.raises(BadPayload, match="zlib") — wt-oneshot/tests/.../test_url_safe.py:108 |
| oneshot | pytest.raises(SignatureExpired, match="< 0 seconds") — wt-oneshot/tests/.../test_timed.py:82 |
| iter2 | assert "Malformed timestamp" in str(excinfo.value) — repeated at test_signer.py:111, 155, test_timed.py:110, 215, test_url_safe.py:55, 67, test_serializer.py:111, test_edge_cases.py:43 |
| iter20 | pytest.raises(BadSignature, match="No") — wt-iter20/tests/.../test_signer.py:101 |
The original (base/tests/.../test_signer.py) asserts on the exception's
payload attribute, not on the message wording. A maintainer rewording any
error message would silently break dozens of generated tests without changing
any contract.
All three suites do "round-trip with locally-recomputed expected value":
- oneshot
test_signer.py:43-47:expected = hmac.new(b"key", msg=b"value", digestmod=hashlib.sha1).digest()— if a mutation brokeHMACAlgorithm.get_signaturein the same way the test mistypes the digest, both fail symmetrically and the test passes. - iter20 has the same shape; iter2 mostly inherits it via
test_signer.py. - The original (
base/tests/.../test_serializer.py:186) pins an exact signed-string byte literal ("[42].-9cNi0CxsSB3hZPNCe9a2eEs1ZM"). That single test catches mutations the generated suites can't, because there's no recomputation to mask the bug.
All three suites contain whole classes that pass a value to a constructor and assert it was stored.
- iter20
test_signer.py:194:assert s.salt == b"itsdangerous.Signer"afterSigner("secret")(asserting the default, not behavior) - iter2
test_signer.py:74-122: per-attribute init assertions for every field - oneshot
test_exc.py:14-97: 9 nearly identical exception-init echo tests + 5issubclass(...)checks
These survive every realistic mutation. The original has no test_exc.py at
all — exceptions are exercised through behavior.
oneshot test_url_safe.py:44-47, 164-167 and iter20 do the same: build a Python
set of "URL-safe characters" inside the test, then assert
set(signed) <= allowed. This is weaker than the original's single
test_digests which pins the exact bytes.
- iter2 imports
_lazy_sha1and_make_keys_list(test_signer.py:13-14) and dedicates a whole test class to_make_keys_list. - iter2 + iter20 both call
s._loads_unsafe_impl(...)directly with internal kwargs. - iter20 tests
SigningAlgorithm()(the abstract base) forNotImplementedError.
The original never reaches for these — it covers their behavior through public API.
Every suite explodes parametrized cases. Example from iter20:
# Original — base/test_signer.py:67-72 — 6 LOC, 4 cases
@pytest.mark.parametrize("key_derivation", ("concat", "django-concat", "hmac", "none"))
def test_key_derivation(self, signer_factory, key_derivation):
signer = signer_factory(key_derivation=key_derivation)
assert signer.unsign(signer.sign("value")) == b"value"
# Generated — iter20 test_signer.py:136-146 — 11 LOC, 3 cases (missing django-concat)
def test_concat_roundtrip(self):
s = Signer("secret", key_derivation="concat")
assert s.unsign(s.sign("hi")) == b"hi"
def test_hmac_roundtrip(self):
s = Signer("secret", key_derivation="hmac")
assert s.unsign(s.sign("hi")) == b"hi"
def test_none_roundtrip(self):
s = Signer("secret", key_derivation="none")
assert s.unsign(s.sign("hi")) == b"hi"The original also reuses entire test classes via inheritance
(TestTimedSerializer(FreezeMixin, TestSerializer)), so every serializer
contract is verified against 4 classes for free. The generated suites
duplicate each class's tests in each file.
The 2 failing tests are both test-side misreads of the source code, not
real bug-finds. Both assume urlsafe_b64decode raises on non-alphabet
characters; it does not — it silently filters them. The author wrote tests
against an imagined contract, not the actual one.
This is the structural cost of one-shot mode: the model can't validate its assumptions against runtime behavior. The 2 failures look like coverage holes but are actually epistemic errors. They'd merge into a real codebase as flaky-or-wrong tests and rot quietly.
iter2 produced a file literally named test_edge_cases.py whose every class
has a docstring stating the source line it targets:
"""signer.py:231-232 — verify_signature returns False when the sig isn't valid base64."""
"""timed.py:114-115 (except: pass) and branch [120, 130] — sig error with an unparseable timestamp."""
"""serializer.py branch [386, 389] — _loads_unsafe_impl called with explicit load_payload_kwargs."""
The serializer test calls the private _loads_unsafe_impl with
load_payload_kwargs={"serializer": json} — but serializer=json is already
the default, so the kwarg has no observable effect. The test would pass even
if load_payload_kwargs were silently dropped. Pure line-flip.
Additionally, test_timed.py:206-215 test_malformed_timestamp_with_bad_sig
uses an or-joined assertion:
assert "Malformed timestamp" in str(excinfo.value) or "does not match" in str(excinfo.value)which accepts either contract outcome. A bug that swapped the two errors goes undetected.
The SUMMARY lists 4 iter_2 additions targeting specific source lines. The audit found:
- 2 of 4 are contract tests (
timed.py:123/126overflow/OSError;timed.py:1349-byte timestampts_int is Nonepath) - 2 of 4 are coverage-chasing:
_MarkerSigner(Signer): passas a "custom signer class" (tautology — verifies the constructor stored its parameter)- The
_loads_unsafe_impl(load_payload_kwargs=...)pattern (same defect as iter2's)
This is interesting: when iter20 stopped at iter_2, the model was making the same kind of mistake as iter2 — chasing branches that don't correspond to real contracts. The extra budget didn't help because the model didn't have the right framing for "what makes a test worth writing".
Across the three audits, the agents converged on three mutations that survive all the generated suites despite 100 % line coverage:
-
timed.py:139(or :148) — changeage > max_agetoage >= max_age. No test pins the boundary caseage == max_age. All three suites use wide gaps (10s vs 0; 120s vs 60; ~24 hours vs 0). -
url_safe.py:60— changelen(compressed) < (len(json) - 1)tolen(compressed) <= (len(json) - 1). No test exercises the boundary; tests use either trivially-small ({"x": 1}, no compression) or trivially-large (1000 repetitions) payloads. -
signer.py:236— replacefor secret_key in reversed(self.secret_keys):with plain iteration. All key-rotation tests still pass because the loop tries every key and only needs one to succeed. No test pins "newest-first" ordering as observable behavior.
A fourth mutation (iter2-audit suggestion): serializer.py:262 — change
payload.decode("utf-8") to payload.decode("ascii"). Survives all suites
because no test exercises a non-ASCII payload through the serializer.
-
High coverage is a lower bound on rigor, not an upper. All three suites passed 97 % easily. None reliably catches the boundary mutations above. The 37-test original would catch some of these via its dense parametrize patterns; the generated 159-176-test suites do not.
-
Iteration budget is bounded by epistemic framing, not by compute. iter20 stopped at iter_2, then produced the same coverage-chasing pattern as iter2. More iterations would have produced more such tests, not better ones. The constraint binding the model isn't "how many tries" — it's "what does it mean for a test to be worth writing".
-
The mock-LOC metric in section 5 is uninformative here. All three suites have 0 mock LOC; itsdangerous is a pure library with no I/O to mock. The metric will be informative on
httpxandrequestsbut is a wash here. Don't draw conclusions from it on this repo.
These are prompt-design changes, not plan changes:
-
Add a "fragility contract" section to the prompt. Explicit guidance like: "Prefer asserting exception type + payload attributes over substring matches on
str(exception). Prefer asserting fixed-byte-string outputs over recomputed-from-the-same-algorithm equality. Do not import private symbols (underscore-prefix or dunder)." -
Add a mutation-aware target. "After you achieve your coverage goal, for each numeric/comparison operator and each conditional in the source, verify at least one test exercises the boundary case (
==,>=vs>, etc.)." This forces the model out of the trivial-payload pattern. -
Stop using line/branch coverage as the iter20 success criterion. Use something like "estimate mutation score on top-N modules" instead. For itsdangerous, line/branch was satisfied in 2 iterations and the remaining 18 iterations would have produced more of the same.
-
For one-shot mode, add an explicit "verify your assumptions" step. "Before writing each test that depends on the runtime behavior of an imported standard-library function, run a small Python REPL check to confirm the function behaves as you expect." Both oneshot failures would have been prevented by running
base64.urlsafe_b64decode(b'!@#$%^&*()')once. -
Make the prompt aware of the original suite's parametrize/inheritance style. The model can produce dense, behavioral suites — it just needs permission to look at the existing tests as a stylistic reference. Adding "if the repository's existing test style uses parametrize and inheritance, mirror that style" would likely cut LOC by ~40 %.
- httpx and requests sessions are still running. When they finish, the same audit pattern applies (3 more agents, 1 per policy per repo). Worth noting: those repos do have I/O, so mock-LOC and fragility patterns will diverge from itsdangerous.
- The mutation-survivor predictions above should be validated by actually
running
mutmuton touched modules. That's the Section 5 work that's still pending.