Files
FabledScribe/tests/test_snippet_merge_provenance.py
T
bvandeusenandClaude Fable 5 77bb3729a3
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 36s
CI & Build / Build & push image (push) Skipped
refactor(tests): per-model fakes, FakeMCP and session mocks come from tests/helpers (#2825, milestone 296 area 1, batch 2)
Second pass over the tests/ ledger after bbee0d0. fake_record(**attrs) is the
one MagicMock-with-real-attributes builder (to_dict mirrors them; the
note-2109 hazard documented once); fake_note/fake_task/fake_snippet/
fake_project/fake_milestone/fake_system/fake_rulebook/fake_topic/fake_rule
carry each model's ordinary defaults on top of it, replacing 14 per-file
factories (two rulebook trios in tool-vs-service wordings, _fake_task, _fake_ms,
_fake_project, _plan_note, _fake_snippet, two _snippet adapters now one-liners
over fake_snippet). FakeMCP replaces the five closure-over-a-list registrar
fakes (+ _Recorder); loc() and design_token_stub() replace the paired _loc /
_token / _T stand-ins; every hand-built async_session mock (9 helper defs and
14 inline copies) now starts from make_mock_session(). Call sites rewritten by
AST with each file's former defaults made explicit, so behaviour is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 11:13:17 -04:00

105 lines
4.6 KiB
Python

"""Merge records what it absorbed (#2087).
Merging keeps the target's scalar fields, unions locations + tags, and trashes
the sources. Without provenance the fact that a variant ever existed survives
only in the trash — and only for someone who already knew to go looking. So the
survivor carries `merged_from`, written to the body and the indexed mirror from
one value, and ordinary edits must carry it forward rather than erase it.
"""
from unittest.mock import AsyncMock, patch
import pytest
from scribe.services import snippets as s
from tests.helpers import fake_snippet
def _snippet(id=1, owner=9, body=None, data=None, tags=None):
return fake_snippet(
id=id, user_id=owner, title="formatDuration — humanize a millisecond count",
body=body if body is not None else s.compose_body(code="x = 1", language="ts"),
tags=tags if tags is not None else ["ts", "snippet"], data=data,
)
async def _run_merge(target, sources, source_ids):
"""Drive merge_snippets with the DB stubbed out; return update_note's kwargs."""
by_id = {target.id: target, **{x.id: x for x in sources}}
async def fake_get(_uid, sid):
return by_id.get(sid)
with patch.object(s, "get_snippet", AsyncMock(side_effect=fake_get)), \
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=target)) as mock_update, \
patch("scribe.services.trash.delete", AsyncMock(return_value=object())):
await s.merge_snippets(7, target.id, source_ids)
return mock_update.await_args.kwargs
@pytest.mark.asyncio
async def test_merge_records_what_it_absorbed_in_body_and_mirror():
kwargs = await _run_merge(
_snippet(id=1), [_snippet(id=2), _snippet(id=3)], [2, 3],
)
assert "**Merged from:** #2, #3" in kwargs["body"]
assert s.merged_from_ids(kwargs["data"]["merged_from"]) == [2, 3]
@pytest.mark.asyncio
async def test_merge_accumulates_across_merges():
"""A target merged twice keeps both histories — the second merge must not
replace the record of the first."""
target = _snippet(id=1, data=s.compose_data(name="f", merged_from=[2]))
kwargs = await _run_merge(target, [_snippet(id=3)], [3])
assert s.merged_from_ids(kwargs["data"]["merged_from"]) == [2, 3]
assert "**Merged from:** #2, #3" in kwargs["body"]
@pytest.mark.asyncio
async def test_merge_does_not_record_a_skipped_cross_owner_source():
"""A source owned by someone else is skipped, not folded in (#231) — so it
must not appear in the provenance either, or the record would claim to
contain something it never absorbed."""
kwargs = await _run_merge(
_snippet(id=1, owner=9),
[_snippet(id=2, owner=9), _snippet(id=3, owner=42)],
[2, 3],
)
assert s.merged_from_ids(kwargs["data"]["merged_from"]) == [2]
assert "#3" not in kwargs["body"]
@pytest.mark.asyncio
async def test_an_ordinary_edit_carries_provenance_forward():
"""Only a merge may add to it, but nothing else may drop it — update
recomposes body and mirror from scratch, so an omission here would silently
erase the history on the next unrelated edit."""
note = _snippet(id=1, data=s.compose_data(name="f", language="ts",
merged_from=[2, 3]))
with patch.object(s, "get_snippet", AsyncMock(return_value=note)), \
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=note)) as mock_update:
await s.update_snippet(7, 1, signature="f(ms) -> string")
kwargs = mock_update.await_args.kwargs
assert s.merged_from_ids(kwargs["data"]["merged_from"]) == [2, 3]
assert "**Merged from:** #2, #3" in kwargs["body"]
@pytest.mark.asyncio
async def test_a_pre_0070_row_keeps_provenance_through_the_body():
"""Rows written before the `data` column are never backfilled, so the body
line is their only copy — and it has to survive an edit too."""
body = s.compose_body(code="x = 1", language="ts", merged_from=[5])
note = _snippet(id=1, body=body, data=None)
with patch.object(s, "get_snippet", AsyncMock(return_value=note)), \
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=note)) as mock_update:
await s.update_snippet(7, 1, when_to_use="humanize a ms count")
assert "**Merged from:** #5" in mock_update.await_args.kwargs["body"]