From 96047453cbe7ffd84f58ff866fb5f30e6c91d9e3 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 6 Oct 2026 01:33:21 +0200 Subject: [PATCH] R-426: decoys for the release-complete gate COVERS "release-complete": the working-tree gate runs in a scratch clone whose origin is a scratch bare repo, against the fake Gitea. Convicted: the newest `## vX.Y.Z` with no tag anywhere, a tag parked on an unrelated commit, a tag with no package, and no-tag wins over a registry 500. Inconclusive: a registry 500. Passed: the genuine release, an `## Unreleased` heading above it, a newer version named only in prose or under `###` (the withdrawn sweep decoy, now asserted the right way round), a tag only origin has (the shallow-CI shape), and a LOCAL-only tag BY DESIGN (CI's fresh clone and the published gate's converse probe see it). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- scripts/test_gate_decoys.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index 63e5ad0..a8ac8cd 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -52,6 +52,11 @@ COVERS = { "lexical sorting would drop out of the retention window (0.9.x vs 0.10.x); a tags api answering 500 " "or a non-JSON 200 is INCONCLUSIVE, never a pass - vs a clean registry, a non-semver tag and a version " "older than the retention window (not asserted, BY DESIGN) (R-426)", + "release-complete": "the newest `## vX.Y.Z` with no tag anywhere, a tag parked on an unrelated commit, a tag " + "with no package; a registry 500 is INCONCLUSIVE - vs the genuine release, an `## Unreleased` " + "heading above it, a newer version named only in prose or under `###`, a tag that only " + "origin has (the shallow-CI shape); a LOCAL-only tag passes BY DESIGN (CI's fresh clone " + "and the published gate's converse probe are what see it) (R-426)", } fails = [] @@ -211,6 +216,68 @@ def published_cases(base): report("published: INCONCLUSIVE: Gitea unreachable", rc, out, 2, ("INCONCLUSIVE", "URLs tried")) +# ── release-complete ───────────────────────────────────────────────────────────────────────────── +def release_cases(base, ws): + bare = os.path.join(ws, "origin.git") + work = os.path.join(ws, "rc-work") + sh(["git", "clone", "-q", "--bare", "--no-tags", "file://" + ROOT, bare], ws) + sh(["git", "clone", "-q", "--no-tags", "file://" + bare, work], ws) + sh(["git", "config", "user.email", "decoy@gate.invalid"], work) + sh(["git", "config", "user.name", "decoy"], work) + # the WORKING-TREE gate, so the file under test is the one being edited, not HEAD's + shutil.copy(os.path.join(ROOT, "scripts", "check-release-complete.py"), + os.path.join(work, "scripts", "check-release-complete.py")) + sh(["git", "add", "scripts/check-release-complete.py"], work) + sh(["git", "commit", "-q", "--allow-empty", "-m", "the gate under test"], work) + base_sha = sh(["git", "rev-parse", "HEAD"], work) + ch = os.path.join(work, "CHANGELOG.md") + original = io.open(ch, encoding="utf-8").read() + V = "9.9.9" + + def case(name, top, expect_rc, must=(), tag=None, origin_tag=False, packaged=True, pkg_status=None): + FAKE.reset() + if packaged: + FAKE.packages = {V} + FAKE.pkg_status = pkg_status + try: + io.open(ch, "w", encoding="utf-8").write(top + original) + sh(["git", "commit", "-q", "-am", name], work) + if tag == "head": + sh(["git", "tag", "-a", "v" + V, "-m", "decoy", "HEAD"], work) + elif tag == "unrelated": + empty = sh(["git", "mktree"], work) # stdin is "" — the empty tree, written to this repo + orphan = sh(["git", "commit-tree", "-m", "unrelated", empty], work) + sh(["git", "tag", "-a", "v" + V, "-m", "decoy", orphan], work) + if origin_tag: + sh(["git", "push", "-q", "origin", "HEAD:refs/tags/v" + V], work) + rc, out = run([sys.executable, os.path.join(work, "scripts", "check-release-complete.py")], + work, child_env(base)) + report("release-complete: " + name, rc, out, expect_rc, must) + finally: + run(["git", "tag", "-d", "v" + V], work) + run(["git", "push", "-q", "origin", ":refs/tags/v" + V], work) + sh(["git", "reset", "-q", "--hard", base_sha], work) + + HEAD = "## v%s — 2026-10-06\n\n- decoy release\n\n" % V + case("GENUINE: tagged at HEAD and published", HEAD, 0, + ("newest CHANGELOG version: v9.9.9", "is tagged, placed and published"), tag="head") + case("GENUINE: an `## Unreleased` heading above the release", "## Unreleased\n\n- wip\n\n" + HEAD, 0, + ("newest CHANGELOG version: v9.9.9",), tag="head") + case("GENUINE: a newer version named only in prose and under ###", + "The `## v10.0.0` heading is not written yet.\n### v10.0.0 notes\n\n" + HEAD, 0, + ("newest CHANGELOG version: v9.9.9",), tag="head") + case("GENUINE: the tag only on origin (the shallow-CI shape)", HEAD, 0, + ("exists on origin",), origin_tag=True) + case("BY DESIGN: a LOCAL-only tag passes (CI's fresh clone sees only origin)", HEAD, 0, + ("an ancestor of HEAD",), tag="head") + case("FACT: the newest heading has no tag anywhere", HEAD, 1, ("DOES NOT EXIST",)) + case("FACT: a tag parked on an unrelated commit", HEAD, 1, ("NOT an ancestor",), tag="unrelated") + case("FACT: tagged, never published", HEAD, 1, ("IS NOT PUBLISHED",), tag="head", packaged=False) + case("INCONCLUSIVE: the registry answers 500", HEAD, 2, ("INCONCLUSIVE",), tag="head", pkg_status=500) + case("FACT beats INCONCLUSIVE: no tag AND the registry answers 500", HEAD, 1, ("DOES NOT EXIST",), + pkg_status=500) + + def main(): srv = Server(("127.0.0.1", 0), Handler) threading.Thread(target=srv.serve_forever, daemon=True).start() @@ -219,6 +286,7 @@ def main(): print("agent gate decoys — fake Gitea at %s, scratch %s" % (base, ws)) try: published_cases(base) + release_cases(base, ws) finally: srv.shutdown() srv.server_close()