eb400a521b
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
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""Unit tests for the snippet serialize/parse helpers (pure functions, no DB)."""
|
|
from scribe.services import snippets as s
|
|
|
|
|
|
def test_compose_title_with_and_without_usage():
|
|
assert s.compose_title("debounce", "rate-limit a callback") == "debounce — rate-limit a callback"
|
|
assert s.compose_title(" debounce ", "") == "debounce"
|
|
assert s.compose_title("debounce") == "debounce"
|
|
|
|
|
|
def test_compose_tags_lowercases_language_and_dedups():
|
|
assert s.compose_tags("Python", ["util", "python"]) == ["python", "snippet", "util"]
|
|
assert s.compose_tags("", None) == ["snippet"]
|
|
assert s.compose_tags("vue", ["snippet"]) == ["vue", "snippet"]
|
|
|
|
|
|
def test_compose_body_includes_fields_and_fence():
|
|
body = s.compose_body(
|
|
code="return 1", language="python", signature="f() -> int",
|
|
when_to_use="always", repo="scribe", path="a.py", symbol="f",
|
|
)
|
|
assert "**When to use:** always" in body
|
|
assert "**Signature:** `f() -> int`" in body
|
|
assert "`scribe` · `a.py` · `f`" in body
|
|
assert "```python\nreturn 1\n```" in body
|
|
assert body.rstrip().endswith("```")
|
|
|
|
|
|
def test_compose_body_bare_code_only():
|
|
body = s.compose_body(code="x = 1")
|
|
assert body.strip() == "```\nx = 1\n```"
|
|
|
|
|
|
def test_parse_round_trips_a_composed_snippet():
|
|
title = s.compose_title("useDebouncedRef", "debounce a reactive ref")
|
|
body = s.compose_body(
|
|
code="const x = 1", language="ts", signature="useDebouncedRef(v, ms)",
|
|
when_to_use="debounce a reactive ref", repo="scribe",
|
|
path="frontend/src/composables/x.ts", symbol="useDebouncedRef",
|
|
)
|
|
got = s.parse_snippet_fields(title, body, ["ts", "snippet"])
|
|
assert got["name"] == "useDebouncedRef"
|
|
assert got["when_to_use"] == "debounce a reactive ref"
|
|
assert got["signature"] == "useDebouncedRef(v, ms)"
|
|
assert got["language"] == "ts"
|
|
assert got["code"] == "const x = 1"
|
|
assert got["repo"] == "scribe"
|
|
assert got["path"] == "frontend/src/composables/x.ts"
|
|
assert got["symbol"] == "useDebouncedRef"
|
|
|
|
|
|
def test_parse_is_tolerant_of_plain_body():
|
|
got = s.parse_snippet_fields("just a name", "no structure here", None)
|
|
assert got["name"] == "just a name"
|
|
assert got["when_to_use"] == ""
|
|
assert got["signature"] == ""
|
|
assert got["code"] == "" # never raises on an unstructured body
|
|
|
|
|
|
def test_parse_falls_back_to_tag_for_language():
|
|
got = s.parse_snippet_fields("n — u", "```\ncode\n```", ["ruby", "snippet"])
|
|
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"
|
|
body = "```js\ncode\n```\n"
|
|
tags = ["js", "snippet"]
|
|
|
|
def to_dict(self):
|
|
return {"id": 1, "title": self.title, "note_type": "snippet", "tags": self.tags}
|
|
|
|
data = s.snippet_to_dict(FakeNote())
|
|
assert data["snippet"]["name"] == "debounce"
|
|
assert data["snippet"]["language"] == "js"
|
|
assert data["snippet"]["code"] == "code"
|