fix(tests): the rule-33 contract follows the scope key into the shared helper (#4085)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m29s
CI & Build / Build & push image (push) Successful in 21s

Two interface-contract tests read each hook's own source for the query args it
sends. The project-scope key now comes from scribe_scope_query, so `repo=` was
no longer spelled in the hook and both read that as the hook having stopped
sending it.

They now pin the pair: the hook delegates to the helper, and the helper emits
exactly `repo=` and `project_id=`. That is a stronger assertion than the one it
replaces — it catches a hook rolling its own scope resolution again, and it
pins the new key, which the route must read for a non-git session to reach its
project at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-16 08:34:55 -04:00
co-authored by Claude Opus 5
parent aa94c73d9e
commit dd08f9858c
2 changed files with 23 additions and 4 deletions
+15 -2
View File
@@ -735,14 +735,27 @@ def test_the_hook_and_the_route_agree_on_every_parameter_name():
import re
hook = Path("plugin/hooks/scribe_tool_rules.sh").read_text()
defs = Path("plugin/hooks/scribe_defs.sh").read_text()
route = Path("src/scribe/routes/plugin.py").read_text()
handler = route.split("async def pre_tool_rules")[1].split("\n@plugin_bp")[0]
sent = set(re.findall(r"[?&]([a-z_]+)=", hook))
assert sent == {"tool", "command", "repo", "exclude_rule_ids"}, sent
assert sent == {"tool", "command", "exclude_rule_ids"}, sent
# `repo` is read by the shared _project_scope() helper, not inline.
# The project-scope key is the shared helper's to choose (#4085): the hook
# splices in `scribe_scope_query`'s output, which is `repo=` inside a git
# repo and `project_id=` where a `.scribe` marker names the project. Both
# halves are one contract with the route, so both are pinned.
assert "scribe_scope_query" in hook, "hook no longer asks for a project scope"
assert set(re.findall(r"printf '([a-z_]+)=", defs)) == {"repo", "project_id"}
# Both scope keys are read by the shared _project_scope() helper, not inline.
assert "_project_scope()" in handler
scope = route.split("async def _project_scope")[1].split("\n@plugin_bp")[0]
for arg in ("project_id", "repo"):
assert f'request.args.get("{arg}"' in scope, (
f"the hooks can send {arg!r} and the route never reads it"
)
for arg in ("tool", "command", "exclude_rule_ids"):
assert f'request.args.get("{arg}")' in handler, (
f"the hook sends {arg!r} and the route never reads it"
+8 -2
View File
@@ -931,10 +931,16 @@ def test_route_reads_every_arg_the_hook_sends():
assert f'request.args.get("{arg}"' in src, f"route ignores {arg}"
hook = HOOK.read_text()
for arg in ("path=", "code=", "repo=", "exclude_ids=", "exclude_sync_ids=",
"shapes="):
for arg in ("path=", "code=", "exclude_ids=", "exclude_sync_ids=", "shapes="):
assert arg in hook, f"hook never sends {arg}"
# The project-scope key is no longer spelled in the hook: since #4085 it is
# whichever of `repo=` / `project_id=` scribe_scope_query picks, so the hook
# splices in its output and the helper is the other half of the contract.
assert "scribe_scope_query" in hook, "hook no longer asks for a project scope"
defs = (HOOK.parent / "scribe_defs.sh").read_text()
assert set(re.findall(r"printf '([a-z_]+)=", defs)) == {"repo", "project_id"}
def test_route_resolves_repo_to_a_project_not_to_a_location_filter():
"""A snippet's `repo` is a label the operator typed; the hook sends a git