feat(scribe): multi-location snippet body convention (backward compatible)
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 1m7s
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 1m7s
Step 1 of the snippet-merge milestone (#231). A snippet that unifies N found one-offs carries N locations (one per call site), so `location` becomes a list. Ships on the body-convention — no migration, swappable to the deferred `data` JSONB later (as #227 decision 5 anticipated). - compose_body: renders `**Location:**` for a single location, a `**Locations:**` bullet list for several. Accepts a `locations` list; the single repo/path/symbol params remain as a one-location shorthand (create path + existing callers/tests unchanged). - parse_snippet_fields: reads BOTH the new `**Locations:**` list block AND the legacy single `**Location:**` line (tolerant, never raises); returns a `locations` list and mirrors the first into repo/path/symbol for back-compat (rule #33). - update_snippet: gains a `locations` param — replaces the whole set; else a legacy single triple overlays onto the first location; else kept. - Tests: multi-location round-trip, singular-vs-plural label, legacy single-line parse, normalize dedup/empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pa2EsuB54BuWQ8GfJq9c7t
This commit is contained in:
@@ -62,6 +62,66 @@ def test_parse_falls_back_to_tag_for_language():
|
||||
assert got["language"] == "ruby"
|
||||
|
||||
|
||||
def test_compose_body_multi_location_renders_bullet_list():
|
||||
body = s.compose_body(
|
||||
code="x = 1",
|
||||
locations=[
|
||||
{"repo": "scribe", "path": "a.py", "symbol": "f"},
|
||||
{"repo": "web", "path": "b.ts", "symbol": "g"},
|
||||
],
|
||||
)
|
||||
assert "**Locations:**" in body
|
||||
assert "- `scribe` · `a.py` · `f`" in body
|
||||
assert "- `web` · `b.ts` · `g`" in body
|
||||
assert "**Location:**" not in body.replace("**Locations:**", "")
|
||||
|
||||
|
||||
def test_compose_body_single_location_via_list_uses_singular_label():
|
||||
body = s.compose_body(
|
||||
code="x = 1", locations=[{"repo": "scribe", "path": "a.py", "symbol": "f"}],
|
||||
)
|
||||
assert "**Location:** `scribe` · `a.py` · `f`" in body
|
||||
assert "**Locations:**" not in body
|
||||
|
||||
|
||||
def test_normalize_locations_drops_empty_and_dedups():
|
||||
got = s._normalize_locations([
|
||||
{"repo": "scribe", "path": "a.py", "symbol": "f"},
|
||||
{"repo": "", "path": "", "symbol": ""}, # dropped (empty)
|
||||
{"repo": "scribe", "path": "a.py", "symbol": "f"}, # dropped (dup)
|
||||
{"repo": "web", "path": "", "symbol": ""},
|
||||
])
|
||||
assert got == [
|
||||
{"repo": "scribe", "path": "a.py", "symbol": "f"},
|
||||
{"repo": "web", "path": "", "symbol": ""},
|
||||
]
|
||||
|
||||
|
||||
def test_parse_round_trips_multi_location():
|
||||
body = s.compose_body(
|
||||
code="const x = 1", language="ts",
|
||||
locations=[
|
||||
{"repo": "scribe", "path": "a.ts", "symbol": "f"},
|
||||
{"repo": "web", "path": "b.ts", "symbol": "g"},
|
||||
],
|
||||
)
|
||||
got = s.parse_snippet_fields("n — u", body, ["ts", "snippet"])
|
||||
assert got["locations"] == [
|
||||
{"repo": "scribe", "path": "a.ts", "symbol": "f"},
|
||||
{"repo": "web", "path": "b.ts", "symbol": "g"},
|
||||
]
|
||||
# repo/path/symbol mirror the first location for back-compat.
|
||||
assert (got["repo"], got["path"], got["symbol"]) == ("scribe", "a.ts", "f")
|
||||
|
||||
|
||||
def test_parse_legacy_single_location_line_still_works():
|
||||
# A body written by the pre-multi-location serializer.
|
||||
body = "**Location:** `scribe` · `a.py` · `f`\n\n```py\nx = 1\n```\n"
|
||||
got = s.parse_snippet_fields("n — u", body, None)
|
||||
assert got["locations"] == [{"repo": "scribe", "path": "a.py", "symbol": "f"}]
|
||||
assert (got["repo"], got["path"], got["symbol"]) == ("scribe", "a.py", "f")
|
||||
|
||||
|
||||
def test_snippet_to_dict_includes_parsed_fields():
|
||||
class FakeNote:
|
||||
title = "debounce — rate-limit"
|
||||
|
||||
Reference in New Issue
Block a user