fix(609): close two false-arm holes found in cross-family review

Codex review of the first attempt found both, and both were in the git plumbing
that my unit tests never touched -- they only exercised the pure predicate.

1. HIGH: git's `%s` is the first PARAGRAPH, not the first line. It joins
   consecutive non-blank lines with spaces, so
     `fix: harmless subject`
     `This explains [decisions-edit] on line two.`
   came back as ONE line containing the token and armed it -- the exact
   false-arm this change exists to prevent. The first line is now taken from
   `%B` via `subject_of()`.

2. HIGH: the in-band `\x1f`/`\x1e` field separators were injectable. A subject
   containing a literal `\x1f` was split at the wrong place and its tail read as
   a trailer, arming the token. Framing is now NUL, which git forbids inside a
   commit message and which therefore cannot be injected, with exact-arity
   parsing (fields must be a multiple of three) that refuses to arm otherwise.

Also from the same review:
- Refuse to arm on a `%(trailers:...)` atom echoed literally by a git older than
  2.22, which would otherwise read as a non-empty trailer (exit 0, so `_run`
  returns it rather than None).
- Record corrected: 38 subject-tokened commits in ancestry, not "twenty"; and it
  no longer claims a blanket fail-safe -- `_token_armed` failing is safe, but the
  surrounding `_diff_findings` fails open earlier on an unresolvable merge-base,
  skipping every check. That predates this change.

Adds 8 integration tests that drive `_token_armed` against a real throwaway git
repo -- the gap that let both defects pass. Verified non-vacuous by
reconstructing the old implementation in memory: it arms on both inputs, the new
one does not.

Note `--format` uses git's `%x00` escape, not a literal NUL: a NUL in argv raises
ValueError from subprocess, which broke every diff-engine test until fixed.
This commit is contained in:
2026-07-25 18:25:26 +02:00
parent 4596603020
commit 243bec708d
3 changed files with 147 additions and 16 deletions
+95
View File
@@ -510,3 +510,98 @@ def test_token_NOT_armed_by_body_prose():
def test_token_not_armed_by_empty_or_whitespace_trailer():
assert not dv.token_armed_in("docs: something", "")
assert not dv.token_armed_in("docs: something", " \n ")
# ---- _token_armed INTEGRATION against real git (ersatztv#609 review round 2) ----
#
# The unit tests above only exercise `token_armed_in`. Both false-arm defects found in review
# (git's `%s` folding the first PARAGRAPH, and an injectable in-band separator) lived in the git
# plumbing and passed those tests untouched. These drive the real command.
def _repo(tmp_path, messages: list[str]) -> tuple[str, str]:
"""Build a throwaway repo; return (base_sha, head_sha)."""
import subprocess
def g(*a, **kw):
return subprocess.run(["git", *a], cwd=tmp_path, capture_output=True, text=True, **kw)
g("init", "-q", ".")
g("config", "user.email", "t@e")
g("config", "user.name", "t")
(tmp_path / "f").write_text("base\n")
g("add", "-A")
g("commit", "-qm", "base")
base = g("rev-parse", "HEAD").stdout.strip()
for i, msg in enumerate(messages):
(tmp_path / "f").write_text(f"{i}\n")
g("add", "-A")
g("commit", "-q", "-F", "-", input=msg)
return base, g("rev-parse", "HEAD").stdout.strip()
def _armed(tmp_path, message: str) -> bool:
import os
base, head = _repo(tmp_path, [message])
cwd = os.getcwd()
os.chdir(tmp_path)
try:
return dv._token_armed(base, head)
finally:
os.chdir(cwd)
def test_integration_subject_token_arms(tmp_path):
assert _armed(tmp_path, "fix(460): write null LastScan [decisions-edit]\n\nbody\n")
def test_integration_trailer_arms(tmp_path):
assert _armed(tmp_path, "docs: correct a figure\n\nwhy\n\nDecisions-Edit: the load number was wrong\n")
def test_integration_body_prose_does_not_arm(tmp_path):
assert not _armed(tmp_path, "docs: explain it\n\nThe check matches [decisions-edit] as a substring.\n")
def test_integration_second_line_of_first_paragraph_does_not_arm(tmp_path):
"""git's %s is the first PARAGRAPH, not the first line — it folds line 2 in with a space.
Using %s directly, this message arms the token. It must not: line 2 is body prose.
"""
assert not _armed(tmp_path, "fix: harmless subject\nThis explains [decisions-edit] on line two.\n\nbody\n")
def test_integration_separator_in_subject_cannot_inject(tmp_path):
"""An in-band \\x1f separator was injectable; NUL cannot appear in a commit message."""
assert not _armed(tmp_path, "docs: harmless\x1fsuffix\n\nbody\n")
assert not _armed(tmp_path, "docs: harmless\x1e\x1fsuffix\n\nbody\n")
def test_integration_empty_trailer_does_not_arm(tmp_path):
assert not _armed(tmp_path, "docs: something\n\nbody\n\nDecisions-Edit:\n")
def test_integration_only_a_later_commit_carries_it(tmp_path):
import os
base, head = _repo(tmp_path, ["chore: unrelated\n", "fix: real correction [decisions-edit]\n"])
cwd = os.getcwd()
os.chdir(tmp_path)
try:
assert dv._token_armed(base, head)
finally:
os.chdir(cwd)
def test_integration_unresolvable_refs_do_not_arm(tmp_path):
"""Guard must still RUN when git can't answer; arming on failure would disable it."""
assert not _armed(tmp_path, "docs: x\n") or True # build a repo first
import os
cwd = os.getcwd()
os.chdir(tmp_path)
try:
assert dv._token_armed("nope1", "nope2") is False
finally:
os.chdir(cwd)