Skip to content

Commit b2234ae

Browse files
earayuclaude
andcommitted
fix(compat): task #61 P1 — fold huangheng+ziang NIT into bulk_upsert tests
Two non-blocking NITs from @huangheng msg=99b5ffd5 + @ziang msg=84f5c3cc re-CR on PR #1927 — fold-in to land more complete test: * `_rejects_mixed_names` now also asserts post-raise zero-side-effect (`get_entity("Alice") is None` + `get_entity("Bob") is None`) — pins Lesson #12 v6.4 aggregation-chain invariant: a backend that swapped validation order to raise AFTER the first row write would silently leak partial state. * New `_replay_is_idempotent` case — pins the Protocol's "Forward-only retry safety: per-part dedup so replays are idempotent" contract. A backend that appended on replay (instead of dedup-then-replace) would silently duplicate lineage members under retry. Coverage delta: 37 → 38 cross-backend cases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e98ce51 commit b2234ae

1 file changed

Lines changed: 34 additions & 0 deletions

File tree

tests/integration/compat/test_lineage_graph_compat.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,13 @@ async def test_bulk_upsert_entity_with_lineage_parts_rejects_mixed_names(store,
883883
(_entity("Bob"), _LM_B_V1),
884884
],
885885
)
886+
# Per @huangheng msg=99b5ffd5 + @ziang msg=84f5c3cc NIT — Lesson
887+
# #12 v6.4 (aggregation chain): a backend that raised AFTER writing
888+
# the first row would silently leak partial state. Pin
889+
# "raise-then-zero-side-effect" so any backend that swaps validation
890+
# order regresses loudly.
891+
assert await s.get_entity("Alice") is None, "raise must occur before any row write"
892+
assert await s.get_entity("Bob") is None, "raise must occur before any row write"
886893

887894

888895
@pytest.mark.asyncio
@@ -985,3 +992,30 @@ async def test_bulk_upsert_entity_with_lineage_parts_entity_type_last_wins(store
985992
got = await s.get_entity("Alice")
986993
assert got is not None
987994
assert got.entity_type == "researcher", f"last bulk part's entity_type wins; got {got.entity_type}"
995+
996+
997+
@pytest.mark.asyncio
998+
async def test_bulk_upsert_entity_with_lineage_parts_replay_is_idempotent(store, collection_id):
999+
"""Per Protocol contract `Forward-only retry safety: the dedup key
1000+
is per-part, so a mid-flight crash + retry replays each tuple
1001+
idempotently`. Two consecutive bulk calls with the same key MUST
1002+
collapse to one lineage member (idempotent) AND the second call's
1003+
description MUST overwrite the first (last-wins on replay).
1004+
1005+
Per @huangheng msg=99b5ffd5 + @ziang msg=84f5c3cc NIT — replay
1006+
safety is the critical retry-correctness invariant; a backend that
1007+
appended on replay (instead of dedup-then-replace) would silently
1008+
duplicate lineage members under retry.
1009+
"""
1010+
1011+
_, s = store
1012+
await s.bulk_upsert_entity_with_lineage_parts(
1013+
parts=[(_entity("Alice", description="v1-first"), _LM_A_V1)],
1014+
)
1015+
await s.bulk_upsert_entity_with_lineage_parts(
1016+
parts=[(_entity("Alice", description="v1-replay"), _LM_A_V1)],
1017+
)
1018+
got = await s.get_entity("Alice")
1019+
assert got is not None
1020+
matching = [lm for lm in got.source_lineage if lm.document_id == "doc-A" and lm.parse_version == "v1"]
1021+
assert len(matching) == 1, f"replay must be idempotent (no duplicate member); got {len(matching)}"

0 commit comments

Comments
 (0)