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
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>
105 lines
4.6 KiB
Python
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"]
|