fix(snippets): close the recall-surface gaps found reviewing the Drafter
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 1m7s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 1m7s
Four defects from the 2026-07-25 review of the recall (#227) and merge (#231) milestones. The theme: a snippet could be recorded but not fully corrected, and the agent and web surfaces had drifted apart. - #2076 language was mis-derived from the first caller tag, so a snippet created with tags and no language read that tag back as its language — corrupting the tag set and the code fence on the next update. Only the FIRST tag can carry the language, since compose_tags emits [language, "snippet", *caller]. - #2077 MCP update_snippet mapped "" to "unchanged", so no field could ever be cleared and no snippet detached from its project. Now an omitted field is left alone, an empty string clears, and project_id follows the -1 = detach convention. A service-level UNSET sentinel keeps None available as the clear. - #2078 surface parity: adds delete_snippet (MCP had none, so a wrong snippet could not be retired by the agent that recorded it), locations on MCP create and update, system_ids through the REST routes and the editor, and the near-duplicate gate on REST create with a "record it anyway" escape. - #2079 project scoping: list_snippets takes project_id through the service, the MCP tool and the REST route, defaulting to every project — reaching across projects is the point when the helper you need was written elsewhere. Sharing the list across owners is deliberately NOT in here: query_knowledge is shared with the Knowledge browse surface, so widening it changes behaviour well beyond snippets. Left open on #2079 for a scope decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt
This commit is contained in:
@@ -27,12 +27,51 @@ def test_snippet_handlers_callable():
|
||||
def test_service_functions_take_user_id():
|
||||
"""Routes must call snippet services with user_id — verify the contract."""
|
||||
from scribe.services import snippets as svc
|
||||
for fn_name in ("create_snippet", "list_snippets", "get_snippet", "update_snippet"):
|
||||
for fn_name in (
|
||||
"create_snippet", "list_snippets", "get_snippet", "update_snippet",
|
||||
"delete_snippet", "merge_snippets",
|
||||
):
|
||||
fn = getattr(svc, fn_name)
|
||||
assert callable(fn)
|
||||
assert "user_id" in inspect.signature(fn).parameters
|
||||
|
||||
|
||||
def test_agent_and_web_surfaces_stay_at_parity():
|
||||
"""The MCP tools and the REST routes are two callers of one service; a
|
||||
capability on one has to exist on the other (rule #33). This guard exists
|
||||
because they drifted apart once: MCP had no delete or `locations`, and the
|
||||
web side had no `system_ids` and no near-duplicate gate."""
|
||||
from scribe.mcp.tools import snippets as tools
|
||||
from scribe.routes import snippets as routes
|
||||
|
||||
# Every write verb the web surface offers, the agent surface offers too.
|
||||
for verb in ("create", "get", "list", "update", "delete", "merge"):
|
||||
assert callable(getattr(tools, f"{verb}_snippets", None) or
|
||||
getattr(tools, f"{verb}_snippet", None)), f"MCP lacks {verb}"
|
||||
|
||||
# Multi-location records are reachable from both.
|
||||
assert "locations" in inspect.signature(tools.create_snippet).parameters
|
||||
assert "locations" in inspect.signature(tools.update_snippet).parameters
|
||||
assert "locations" in inspect.getsource(routes.create_snippet_route)
|
||||
assert "locations" in inspect.getsource(routes.update_snippet_route)
|
||||
|
||||
# System association and the duplicate gate reach both.
|
||||
assert "system_ids" in inspect.signature(tools.create_snippet).parameters
|
||||
for route in (routes.create_snippet_route, routes.update_snippet_route):
|
||||
assert "system_ids" in inspect.getsource(route)
|
||||
assert "find_duplicate_note" in inspect.getsource(routes.create_snippet_route)
|
||||
|
||||
|
||||
def test_project_scoping_reaches_every_caller():
|
||||
"""A snippet search has to be narrowable to one project from both surfaces."""
|
||||
from scribe.mcp.tools import snippets as tools
|
||||
from scribe.routes import snippets as routes
|
||||
from scribe.services import snippets as svc
|
||||
assert "project_id" in inspect.signature(svc.list_snippets).parameters
|
||||
assert "project_id" in inspect.signature(tools.list_snippets).parameters
|
||||
assert "project_id" in inspect.getsource(routes.list_snippets_route)
|
||||
|
||||
|
||||
def test_update_field_map_matches_service_kwargs():
|
||||
"""Every field the PATCH route forwards must be a real update_snippet kwarg
|
||||
(rule #33 interface-contract parity)."""
|
||||
|
||||
Reference in New Issue
Block a user