fix(plugin): write-path hooks say when Scribe did not answer — once per outage, shared marker, record nudge withheld on an unanswered call; check_plugin allows exactly that line when unreachable; plugin 0.1.43 (#2932)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 13s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 40s
CI & Build / Python tests (push) Failing after 49s
CI & Build / Build & push image (push) Skipped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 10:59:54 -04:00
co-authored by Claude Fable 5
parent 9c00a4b6e1
commit 0ab94b2a00
8 changed files with 208 additions and 40 deletions
+67 -16
View File
@@ -909,29 +909,35 @@ def _hook_runtime_env():
"SCRIBE_URL": "http://127.0.0.1:9", "SCRIBE_TOKEN": "t"}
def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_path):
"""#2664: the local arm proves duplication; when Scribe has no record of it,
the same context block must ask for create_snippet — the one moment the
recording nudge is earned rather than noise. An unreachable server counts
as "nothing recorded": the local finding needed no server, and the nudge
fails open with it (here: a refused connection stands in for the instance)."""
env = _hook_runtime_env()
def _dup_repo(tmp_path, env):
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True, env=env)
(repo / "a.py").write_text("def debounce(fn):\n return fn\n")
# git grep searches the index, so the existing copy must be staged.
subprocess.run(["git", "add", "."], cwd=repo, check=True, env=env)
out = subprocess.run(
["bash", str(HOOK)],
input=json.dumps({
"session_id": "s-nudge", "cwd": str(repo), "tool_name": "Write",
"tool_input": {"file_path": str(repo / "b.py"),
"content": "def debounce(fn):\n return fn\n"},
}),
capture_output=True, text=True, env=env,
)
return repo
def _write_event(repo, session="s-nudge"):
return json.dumps({
"session_id": session, "cwd": str(repo), "tool_name": "Write",
"tool_input": {"file_path": str(repo / "b.py"),
"content": "def debounce(fn):\n return fn\n"},
})
def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_path):
"""#2664: the local arm proves duplication; when Scribe ANSWERS that it has
no record of it, the same context block must ask for create_snippet — the
one moment the recording nudge is earned rather than noise."""
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, seen):
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}")
repo = _dup_repo(tmp_path, env)
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo),
capture_output=True, text=True, env=env)
assert out.returncode == 0
assert seen and seen[0]["path"] == ["b.py"]
assert out.stdout.strip(), (
"hook produced no output — the local arm should have found the "
"staged duplicate and nudged"
@@ -939,6 +945,51 @@ def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_pat
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
assert "already defined" in ctx # the duplication finding
assert "create_snippet" in ctx # the recording ask riding it
assert "did not answer" not in ctx
def test_hook_says_when_scribe_did_not_answer_once_per_outage(tmp_path):
"""#2932: a configured instance that does not answer (refused connection)
is SAID — the write went unchecked — instead of the hook failing open in
silence; the record nudge's "nothing recorded" claim is withheld. Once per
outage: a second miss is quiet, an answer clears the marker, and the next
miss speaks again. The marker is shared with the after-write hook."""
env = _hook_runtime_env() # SCRIBE_URL → a refused port
repo = _dup_repo(tmp_path, env)
marker = tmp_path / "scribe-priorart" / "s-out.unreached"
env["TMPDIR"] = str(tmp_path)
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
capture_output=True, text=True, env=env)
assert out.returncode == 0
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
assert "already defined" in ctx
assert "Scribe did not answer the prior-art check for `b.py` within 5s" in ctx
assert "UNCHECKED" in ctx and "list_shapes" in ctx
assert "None of those existing copies is recorded" not in ctx
assert marker.is_file()
# Second miss inside the quiet window: local arm only.
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
capture_output=True, text=True, env=env)
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
assert "already defined" in ctx and "did not answer" not in ctx
# An answer clears the marker …
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, _seen):
up = dict(env, SCRIBE_URL=f"http://127.0.0.1:{port}")
subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
capture_output=True, text=True, env=up)
assert not marker.exists()
# … so the next outage is announced afresh.
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
capture_output=True, text=True, env=env)
assert "did not answer" in json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
# A write the hook had nothing local to say about still carries the line
# (the line is the whole message then): a fresh session, no duplicate.
(repo / "a.py").unlink()
subprocess.run(["git", "add", "-A"], cwd=repo, check=True, env=env)
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out-2"),
capture_output=True, text=True, env=env)
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
assert ctx.startswith("> Scribe did not answer")
def test_hook_stays_quiet_about_recording_when_nothing_is_duplicated(tmp_path):