Files
FabledGrimoire/tests/test_hook_duplicate_confirmation.py
T
bvandeusenandClaude Opus 5.5 17e1931c0b
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 13s
docs(6209): rulebooks, pull telemetry, lesson usage, duplicate arm, cited status, trigger shape
Six more test files state their pinned properties without the issue and
milestone history: the pull-telemetry guard explains enumerating kinds
instead of listing two getter incidents, cited-record status explains why
a hint's id needs its status instead of recounting one misreported step,
and the trigger-shape guard keeps its reasoning about why prose heuristics
fail while dropping the dated corpus audit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:28:35 -04:00

223 lines
9.5 KiB
Python

"""The by-name duplicate arm confirms its grep hits against the real extractor.
The extractors that decide what a payload DEFINES blank comment and string
spans before any line matcher runs. This is the other end — `scribe_local_dups`,
which decides which OTHER files already define that name — and a plain
`git grep` cannot do that.
A grep sees lines, not spans. The line
class with only modifier rules is a deletion that went half-way.
is prose from a module docstring in this repo, and it matches the arm's pattern
for `name=with`. So the arm would tell a writer their genuine symbol was
already defined, and point at a docstring.
WHAT CONFIRMATION COSTS, measured rather than guessed: it drops string
literals, comments, import statements and Vue's `const emit = defineEmits()`
boilerplate, and no real definition — where a name has both a real definition
and a phantom, the phantom goes and the definition stays. The arm is
dominated by its `git grep` calls, so confirming adds about 15% (a mean of
~30ms) rather than the second a per-(name, file) shape would cost.
These cases are written against real git repositories rather than mocks,
because what is being pinned is the interaction between `git grep`, `head` and
the extractor — which is also where a `pipefail` + `head` interaction lives.
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
DEFS = ROOT / "plugin" / "hooks" / "scribe_defs.sh"
@pytest.fixture
def repo(tmp_path):
"""A committed git repo, plus a runner for the arm against it."""
for tool in ("git", "bash", "awk"):
if shutil.which(tool) is None:
pytest.skip(f"{tool!r} not installed")
env = {"PATH": os.environ["PATH"], "HOME": str(tmp_path),
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@x",
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@x"}
root = tmp_path / "repo"
root.mkdir()
subprocess.run(["git", "init", "-q"], cwd=root, check=True, env=env)
def build(files: dict[str, str]):
for rel, text in files.items():
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
subprocess.run(["git", "add", "-A"], cwd=root, check=True, env=env,
capture_output=True)
subprocess.run(["git", "commit", "-q", "-m", "base"], cwd=root,
check=True, env=env, capture_output=True)
def run(names: str, writing: str = "new.py") -> str:
script = (f'set -uo pipefail\n. "{DEFS}"\n'
f'printf %s "$NAMES" | scribe_local_dups "{root}" {writing}\n')
out = subprocess.run(["bash", "-c", script], capture_output=True,
text=True, env={**env, "NAMES": names}, timeout=120)
assert out.returncode == 0, out.stderr
return out.stdout
return build, run
# ── the case the task names ──────────────────────────────────────────────────
def test_a_sentence_about_a_definition_is_not_a_definition(repo):
"""One real `def change`, one docstring that
wraps onto a line beginning `function change`. The writer is told about
the first file and not the second."""
build, run = repo
build({
"real.py": "def change(a, b):\n return a\n",
"prose.py": (
'"""Some module.\n\n'
'When a caller moves, the enclosing\n'
'function change may have moved with it, which is the case\n'
'this paragraph exists to describe.\n'
'"""\n\n'
'X = 1\n'
),
})
out = run("sym\tchange\n")
assert "real.py" in out
assert "prose.py" not in out, (
"a wrapped docstring line still reads as a definition — the "
"confirmation pass did not run, or did not see the span"
)
assert "in 1 other file(s)" in out
def test_the_repos_own_prose_no_longer_defines_with():
"""Not a fixture — this repo's actual corpus, which is where the defect
was found.
Before the confirmation pass the arm named four files for `with`:
`…record type with no vector…`, `A class with no rules anywhere…` and two
more, all of them prose in docstrings and comments. It now names none,
and none is the correct answer: nothing here defines a symbol called
`with`, and in several of the languages in this tree it could not, because
it is a keyword.
Asserting on emptiness is only honest if the assertion could see a hit —
so it checks the arm ran, via a name that IS defined here.
"""
if shutil.which("bash") is None:
pytest.skip("bash not installed")
def arm(name: str) -> str:
script = (f'set -uo pipefail\n. "{DEFS}"\n'
f'printf "sym\\t{name}\\n" | scribe_local_dups "{ROOT}" nothing.py\n')
out = subprocess.run(["bash", "-c", script], capture_output=True,
text=True, timeout=180)
assert out.returncode == 0, out.stderr
return out.stdout
assert arm("extract_definitions").strip(), (
"the arm found nothing for a name this repo certainly defines, so a "
"silent result below would prove nothing"
)
assert arm("with").strip() == "", (
"`with` is prose in every file that matches the grep — a hit here is "
"a sentence being read as a definition"
)
def test_a_name_that_is_both_real_and_phantom_keeps_the_real_file(repo):
"""The narrowing case, and the one that would betray an over-eager filter:
the phantom file drops out and the genuine definition stays."""
build, run = repo
build({
"lib.py": "def create_note(user_id):\n return user_id\n",
"test_form.py": 'canon = shape_form("async def create_note(user_id: int, ...):", "sym")\n',
})
out = run("sym\tcreate_note\n")
assert "lib.py" in out
assert "test_form.py" not in out
assert "in 1 other file(s)" in out
def test_an_import_is_not_a_definition(repo):
"""TypeScript's `import { type Foo }` matched the `type\\s+NAME` arm, so a
new type was reported as a duplicate of the files importing it."""
build, run = repo
build({
"card.vue": 'import {\n type Choices, type Decision,\n} from "@/api/inception";\n',
"real.ts": "export type Choices = { a: number };\n",
})
out = run("sym\tChoices\n")
assert "card.vue" not in out, "an import site is not a definition site"
# ── the caps ─────────────────────────────────────────────────────────────────
def test_the_candidate_cap_is_raised_above_the_display_cap():
"""Confirmation REMOVES hits, so a cap applied before it runs lets phantom
matches crowd real definitions out of the window — hits dropped before
anyone looked at them. Asserted on structure because the ordering is what
matters, not the numbers."""
src = DEFS.read_text()
for const in ("_SCRIBE_DUP_CANDIDATES=", "_SCRIBE_DUP_SHOWN="):
assert const in src, f"{const.rstrip(chr(61))} is gone — the two caps were folded back together"
cands = int(src.split("_SCRIBE_DUP_CANDIDATES=")[1].split("\n")[0])
shown = int(src.split("_SCRIBE_DUP_SHOWN=")[1].split("\n")[0])
assert cands > shown, (
f"candidates ({cands}) must exceed the display cap ({shown}), or "
"confirmation can only ever shrink an already-truncated window"
)
def test_phantoms_ahead_of_the_display_cap_do_not_hide_a_real_definition(repo):
"""Four phantom files sort before the one real definition. With the old
`head -4` on the grep, the real file never entered the window at all."""
build, run = repo
files = {f"a_phantom_{i}.py": f'BLURB = """\nclass Widget is described here.\n"""\n'
for i in range(4)}
files["z_real.py"] = "class Widget:\n pass\n"
build(files)
out = run("sym\tWidget\n")
assert "z_real.py" in out, "the real definition sorted behind four phantoms"
assert "a_phantom_0.py" not in out
def test_the_early_exiting_head_still_keeps_its_own_output(repo):
"""Under `pipefail` a `head` that
exits while git grep is still writing must not void the hits it printed.
Every file here holds a REAL definition, so confirmation keeps them all
and only the display cap applies."""
build, run = repo
build({f"module_with_a_fairly_long_descriptive_name_{i}.py": "def slug(t):\n return t\n"
for i in range(3000)})
out = run("sym\tslug\n")
assert "`slug` is already defined in 4 other file(s)" in out
# ── css ──────────────────────────────────────────────────────────────────────
def test_a_selector_named_inside_a_css_comment_is_not_a_rule(repo):
"""A selector inside a CSS comment, on the lookup side rather than the
extraction side."""
build, run = repo
build({
"real.css": ".badge { color: red; }\n",
# The selector must START the line, or the old anchored pattern
# never matched it and this case would pass without the fix.
"notes.css": "/*\n.badge, .chip and friends were retired here\n*/\na { color: blue; }\n",
})
out = run("css\tbadge\n")
assert "real.css" in out
assert "notes.css" not in out