refactor(tests+frontend): one http_sink helper for the hook tests; apiErrorMessage replaces ten hand-rolled error-body parses; type X, import specifiers are not definitions (#2904, milestone 299 step 6)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m11s
CI & Build / Build & push image (push) Successful in 38s

tests/helpers.http_sink replaces three module-local _Sink handlers (the
write-path tests and the after-write test). ProjectView + SettingsView
parsed `(e as {body?:{error?}}).body?.error || fallback` by hand ten times
beside the apiErrorMessage canon (#2853) - all ten now call it. The
extractor (server + the hook awk mirror) no longer reads `import { type Foo }`
as a definition of Foo - that was the last "identical body" sym family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 15:07:23 -04:00
co-authored by Claude Fable 5
parent 449f437048
commit 590203a293
8 changed files with 137 additions and 133 deletions
+26 -53
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from tests.helpers import fake_note
from tests.helpers import fake_note, http_sink
PLUGIN = Path(__file__).resolve().parents[1] / "plugin"
HOOK = PLUGIN / "hooks" / "scribe_prior_art.sh"
@@ -1149,6 +1149,23 @@ def test_route_stamps_only_for_a_caller_allowed_to_write():
assert "&shapes=" in hook
def test_hook_does_not_name_a_type_import_specifier_as_a_shape(tmp_path):
"""#2904, the awk mirror of the server rule: `type Foo,` inside an import
list is not a definition; `type Baz = …` on its own line is."""
env = _hook_runtime_env()
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True, env=env)
seen = _run_hook_against_sink(tmp_path, {
"session_id": "s-type", "cwd": str(repo), "tool_name": "Write",
"tool_input": {"file_path": str(repo / "x.ts"),
"content": 'import { type Foo, bar } from "./y";\n'
"type Baz = { a: number };\n"
"export function use(): Baz {\n return { a: 1 };\n}\n"},
})
assert seen["shapes"] == ["sym:Baz,sym:use"]
def test_hook_names_the_shapes_being_written():
"""The feed's two inputs: every definition in the payload, or — for an Edit
that changes a body, not a signature — the definition enclosing the edit,
@@ -1165,39 +1182,15 @@ def test_hook_names_the_shapes_being_written():
def _run_hook_against_sink(tmp_path, payload):
"""Run the hook with SCRIBE_URL pointed at a throwaway local listener and
return the query the hook sent. Lets the shell be tested end to end —
the extraction, the encoding, the URL — without a Scribe instance."""
import http.server
import threading
import urllib.parse
seen: dict = {}
class _Sink(http.server.BaseHTTPRequestHandler):
def do_GET(self):
seen.update(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query))
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"context":"","note_ids":[]}')
def log_message(self, *a):
pass
server = http.server.HTTPServer(("127.0.0.1", 0), _Sink)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{server.server_port}")
return the query the hook sent (tests.helpers.http_sink)."""
with http_sink() as (port, seen):
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}")
out = subprocess.run(
["bash", str(HOOK)], input=json.dumps(payload),
capture_output=True, text=True, env=env,
)
assert out.returncode == 0, out.stderr
finally:
server.shutdown()
server.server_close()
return seen
return seen[0] if seen else {}
def test_hook_sends_every_definition_in_a_write(tmp_path):
@@ -1321,28 +1314,10 @@ def test_the_hook_keeps_a_derive_channel_and_sends_it_back(tmp_path):
"""#2900: derive keys the server returns land in the session's own
`.derive.ids` file and go back as `exclude_derive` on the next write —
a family is named once per session, not at every edit."""
import http.server
import threading
import urllib.parse
seen: list[dict] = []
class _Sink(http.server.BaseHTTPRequestHandler):
def do_GET(self):
seen.append(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query))
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"context":"> family","note_ids":[],"sync_note_ids":[],'
b'"derive_keys":["dup:483a","canon:2855"]}')
def log_message(self, *a):
pass
server = http.server.HTTPServer(("127.0.0.1", 0), _Sink)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{server.server_port}",
reply = (b'{"context":"> family","note_ids":[],"sync_note_ids":[],'
b'"derive_keys":["dup:483a","canon:2855"]}')
with http_sink(reply) as (port, seen):
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}",
TMPDIR=str(tmp_path))
payload = {"session_id": "s-derive-1", "cwd": str(tmp_path), "tool_name": "Write",
"tool_input": {"file_path": str(tmp_path / "a.css"),
@@ -1351,8 +1326,6 @@ def test_the_hook_keeps_a_derive_channel_and_sends_it_back(tmp_path):
out = subprocess.run(["bash", str(HOOK)], input=json.dumps(payload),
capture_output=True, text=True, env=env)
assert out.returncode == 0, out.stderr
finally:
server.shutdown()
assert "exclude_derive" not in seen[0]
assert seen[1]["exclude_derive"] == ["dup:483a,canon:2855"]
state = tmp_path / "scribe-priorart" / "s-derive-1.derive.ids"