feat(snippets): record merge provenance on the survivor
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 38s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 1m7s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 38s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 1m7s
Merge kept the target's fields, unioned locations and tags, and trashed the sources — recording nothing about what it absorbed. If a variant handled an edge case the survivor doesn't, that difference left the visible record entirely; recovering it meant knowing to go digging in the trash. The survivor now carries `merged_from`: a "**Merged from:** #2, #3" line in the body for humans, and the same list in the `data` mirror for queries, written from one value like every other field (#2087). It accumulates rather than replaces — a target merged twice keeps both histories — and skipped sources (cross-owner, per #231) are excluded, so the record never claims to contain something it never absorbed. Ordinary edits carry it forward. update_snippet recomposes body and mirror from scratch, so an omission there would silently erase the history on the next unrelated edit; that path is pinned by its own test, including the pre-0070 case where the body line is the only copy. Surfaced in the snippet detail view as a "Merged from" row — the view renders parsed fields, not the raw body, so the body line alone would have been invisible to the operator (rule #27). Un-merge, the other half of #2087, stays open: restoring a source from trash still doesn't strip its locations off the survivor, and what partial un-merge should mean is a design question, not a coding one. `merged_from` is the record that makes it tractable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt
This commit is contained in:
@@ -235,20 +235,57 @@ def test_data_and_body_round_trip_to_the_same_fields():
|
||||
# serializers get their own argument lists rather than a shared spread.
|
||||
title = s.compose_title(name, when)
|
||||
body = s.compose_body(code="const x = 1", language=lang, signature=sig,
|
||||
when_to_use=when, locations=locs)
|
||||
when_to_use=when, locations=locs, merged_from=[41, 42])
|
||||
tags = s.compose_tags(lang)
|
||||
|
||||
from_body = s.snippet_fields(_Note(title=title, body=body, tags=tags))
|
||||
from_data = s.snippet_fields(_Note(
|
||||
title=title, body=body, tags=tags,
|
||||
data=s.compose_data(name=name, when_to_use=when, signature=sig,
|
||||
language=lang, locations=locs),
|
||||
language=lang, locations=locs, merged_from=[41, 42]),
|
||||
))
|
||||
for key in ("name", "when_to_use", "signature", "language", "locations",
|
||||
"repo", "path", "symbol", "code"):
|
||||
"merged_from", "repo", "path", "symbol", "code"):
|
||||
assert from_body[key] == from_data[key], key
|
||||
|
||||
|
||||
# --- merge provenance (#2087) ------------------------------------------------
|
||||
|
||||
def test_normalize_merged_from_keeps_history_order_and_drops_junk():
|
||||
"""Order is history, not sorting — earlier merges stay first."""
|
||||
assert s._normalize_merged_from([9, 3, 9, "4", None, 0, -2, "x"]) == [9, 3, 4]
|
||||
assert s._normalize_merged_from(None) == []
|
||||
|
||||
|
||||
def test_compose_body_renders_merged_from_and_parse_reads_it_back():
|
||||
body = s.compose_body(code="x = 1", merged_from=[12, 13])
|
||||
assert "**Merged from:** #12, #13" in body
|
||||
got = s.parse_snippet_fields("n — u", body, None)
|
||||
assert got["merged_from"] == [12, 13]
|
||||
# The code fence still comes last — provenance is header metadata.
|
||||
assert body.rstrip().endswith("```")
|
||||
|
||||
|
||||
def test_compose_body_omits_merged_from_when_there_is_none():
|
||||
"""A snippet that was never merged says nothing about merging."""
|
||||
assert "Merged from" not in s.compose_body(code="x = 1")
|
||||
assert s.parse_snippet_fields("n — u", "```\nx = 1\n```", None)["merged_from"] == []
|
||||
|
||||
|
||||
def test_compose_data_mirrors_merged_from():
|
||||
assert s.compose_data(name="f", merged_from=[3, 3, 2])["merged_from"] == [3, 2]
|
||||
assert "merged_from" not in s.compose_data(name="f")
|
||||
|
||||
|
||||
def test_snippet_fields_prefers_the_data_columns_merged_from():
|
||||
"""Same rule as every other mirrored field: `data` wins when it's populated,
|
||||
so a hand-edited body can't silently rewrite the merge history."""
|
||||
body = s.compose_body(code="x = 1", merged_from=[12])
|
||||
note = _Note(title="n — u", body=body, tags=["snippet"],
|
||||
data=s.compose_data(name="n", merged_from=[12, 13]))
|
||||
assert s.snippet_fields(note)["merged_from"] == [12, 13]
|
||||
|
||||
|
||||
def test_snippet_to_dict_includes_parsed_fields():
|
||||
class FakeNote:
|
||||
title = "debounce — rate-limit"
|
||||
|
||||
Reference in New Issue
Block a user