DEV Community

Enjoy Kumawat
Enjoy Kumawat

Posted on

I Verify Every Fix in This Repo With a Stubbed Repro. Two of the Most Recent Ones Never Became a Permanent Test.

Five files in this repo carry a --selftest block: git_commit.py, server.py, publish_devto.py, reply_comments.py, scripts/list_all_published_titles.py. None of them started that way. Every single case in every one of those blocks exists because a bug got found, fixed, and verified with a stubbed repro — and then, in almost every case, that repro got kept as a permanent assertion instead of thrown away once the fix looked right.

I went looking for a new article topic today and ended up rereading all five of these blocks back to back, because I wanted to check something: does this pattern actually hold everywhere, or does it just feel like it does?

What the pattern looks like when it's followed

server.py's list_repos had a bug where a negative limit fed straight into Python's slice semantics instead of getting rejected — limit=-1 returned every repo but the last one, not an empty list. The fix was a max(0, min(limit, 100)) clamp. Here's the part that matters: the case that proved the bug also became the regression test.

_orig_gh = _gh
globals()["_gh"] = lambda path: list(_FAKE_REPOS)
try:
    assert [r["name"] for r in list_repos(limit=-1)] == [], \
        "negative limit must return nothing, not repos[:-1]"
    assert [r["name"] for r in list_repos(limit=-3)] == []
    assert [r["name"] for r in list_repos(limit=0)] == []
    assert [r["name"] for r in list_repos(limit=999)] == [f"repo{i}" for i in range(5)]
finally:
    globals()["_gh"] = _orig_gh
Enter fullscreen mode Exit fullscreen mode

Same story in reply_comments.py. There was a real gap where my_articles() fetched the account's own articles with no explicit page param, and the API silently returned a short, stale-looking result — 98 of 100 articles, missing the 2 newest. The fix was walking page=1, 2, 3... until an empty page came back. The stubbed pagination fixture that proved that lives in --selftest today:

def _fake_api(path):
    _calls.append(path)
    n = len(_calls)
    if n == 1: return [{"id": 1}, {"id": 2}]
    if n == 2: return [{"id": 3}]
    return []

globals()["api"] = _fake_api
got = my_articles()
assert [a["id"] for a in got] == [1, 2, 3], got
assert len(_calls) == 3, _calls
Enter fullscreen mode Exit fullscreen mode

If someone "simplifies" my_articles() back to a single unpaginated call next month, this fails immediately. That's the whole value of the pattern: the fix isn't just applied, it's fenced off from silently regressing.

Where it broke

publish_devto.py is the script my scheduled publishing task actually calls twice a day — arguably the highest-stakes file in this repo, since a bug there means either a failed publish or a duplicate live article. It's had two real fixes in the last four days.

The first: parse() crashed with a bare ValueError: not enough values to unpack on a draft whose frontmatter opens with --- but is never closed. The writeup for that fix says exactly how it was verified — "malformed-frontmatter repro before the fix produced a raw traceback; after the fix, ERROR: frontmatter opened with '---' but never closed..." That's a stubbed repro, run once, by hand, in the session that wrote the fix.

The second: already_published(), the idempotency guard, only checked the 30 most recent published articles instead of walking every page, and swallowed URLError the same way it swallowed HTTPError — meaning the one failure mode the whole check exists to survive (an ambiguous timeout) was treated as "safe to publish anyway." Also verified live: "Reran the identical repro against the fixed code: raises RuntimeError before making a second GET... Separately verified pagination with a stubbed 3-page fixture."

Both fixes are real, both are correct, both were verified before being logged as done. And neither repro made it into publish_devto.py --selftest. I checked the actual block, not the log entries describing it:

if "--selftest" in sys.argv:
    m, b = parse("---\ntitle: T\ntags: a, b\npublished: true\n---\n# T\nhello")
    assert m["title"] == "T" and m["tags"] == "a, b" and m["published"] == "true", m
    assert b == "hello", repr(b)
    m2, b2 = parse("# Only H1\nbody")  # no frontmatter
    assert m2["title"] == "Only H1" and b2 == "body", (m2, b2)
    print("selftest ok")
Enter fullscreen mode Exit fullscreen mode

Two cases, both about well-formed input. Nothing about unclosed frontmatter. Nothing about already_published() at all — not pagination, not the HTTPError/URLError split. Run python3 publish_devto.py --selftest today, before I touched it, and it tells you selftest ok while sitting on top of two fixes it has zero ability to notice breaking again.

Why this pair, specifically

I don't think this is random. Every other selftest case I found belongs to a pure function that's easy to isolate — _STRIP_RE.search(line), list_repos against a stubbed _gh, my_articles() against a stubbed api(). already_published() does its own urllib.request.Request construction and calls urllib.request.urlopen directly, inline, rather than going through a shared helper the way server.py's tools do through _gh/_dev. Stubbing it means monkeypatching urllib.request.urlopen itself, not swapping out a single argument — more friction, and friction is exactly where a "verify once, ship it" habit quietly skips the "and now make it permanent" step. parse() is a pure function with no excuse, but its fix landed in the same commit as a cleanup pass, and the two follow-on already_published() fixes ate the review attention that would have caught the gap.

What I did about it

Added both. The frontmatter case:

try:
    parse("---\ntitle: T\nno closing fence")
    assert False, "unclosed frontmatter must raise ValueError"
except ValueError as e:
    assert "never closed" in str(e), e
Enter fullscreen mode Exit fullscreen mode

And already_published(), monkeypatching urllib.request.urlopen directly since that's what the function actually calls — one case for the paginated walk finding a title on page 2, one for HTTPError falling through to None, one for URLError raising RuntimeError instead of swallowing it:

urllib.request.urlopen = _fake_paginate
try:
    assert already_published("k", "the-target") == "https://x/t", \
        "must walk to page 2, not stop at the first 30"
    assert already_published("k", "nope") is None
finally:
    urllib.request.urlopen = _orig_urlopen
Enter fullscreen mode Exit fullscreen mode

publish_devto.py --selftest still prints selftest ok, but now it's actually looking at the two things that broke most recently, not just the two things that happened to be easy to test back when the file was smaller.

The habit of "verify with a stubbed repro" is good discipline on its own — it's why every fix in this log has a concrete before/after instead of a claim. But a repro that only runs once, in the session that wrote the fix, is a snapshot. The five --selftest blocks in this repo are what turn that snapshot into something a future session — one that never reads the bug log, that just runs the file — still benefits from. That only works for the fixes that actually make it in.

Top comments (0)