"""resolve.py — resource_key → item id precedence and ambiguity (plan §5). Precedence: local mapping store (authoritative at execution) → resource marker → (setup-time-only matchers/title, never silently applied at runtime). Ambiguous or missing ⇒ ClarificationNeededError listing candidates. """ import pytest from fakes import make_item from mcp_server.errors import ClarificationNeededError async def test_resolve_via_marker_when_store_is_empty(client, real_resolver): """Marker comment in the resource body is discovered without any mapping.""" assert await real_resolver.resolve("ethanol_absolute") == 12 async def test_mapping_store_is_authoritative_over_marker(client, real_resolver): await real_resolver.upsert_mapping("ethanol_absolute", 12) # Even if another item also carries the marker, the store binding wins. client.items[13] = make_item(id=13, title="Ethanol bottle (old)", body="") assert await real_resolver.resolve("ethanol_absolute") == 12 async def test_resolve_unknown_key_raises_clarification(client, real_resolver): with pytest.raises(ClarificationNeededError) as excinfo: await real_resolver.resolve("sodium_azide") assert "sodium_azide" in str(excinfo.value) async def test_resolve_ambiguous_marker_raises_clarification_with_candidates(client, real_resolver): client.items[13] = make_item(id=13, title="Ethanol bottle 2", body="") with pytest.raises(ClarificationNeededError) as excinfo: await real_resolver.resolve("ethanol_absolute") assert excinfo.value.options, "clarification must list candidate resources" async def test_upsert_mapping_is_idempotent(client, real_resolver): await real_resolver.upsert_mapping("ethanol_absolute", 12) await real_resolver.upsert_mapping("ethanol_absolute", 12) mappings = await real_resolver.list_mappings() assert [m.item_id for m in mappings if m.resource_key == "ethanol_absolute"] == [12] async def test_upsert_mapping_updates_existing_binding(client, real_resolver): await real_resolver.upsert_mapping("ethanol_absolute", 12) await real_resolver.upsert_mapping("ethanol_absolute", 99) assert await real_resolver.resolve("ethanol_absolute") == 99 async def test_list_mappings_empty_store(real_resolver): assert await real_resolver.list_mappings() == [] async def test_export_mapping_produces_portable_json(client, real_resolver): await real_resolver.upsert_mapping("ethanol_absolute", 12) data = await real_resolver.export_mapping() assert "ethanol_absolute" in data entry = data["ethanol_absolute"] assert entry.get("title") in (None, "Absolute Ethanol") async def test_import_mapping_returns_proposals(client, real_resolver): data = {"ethanol_absolute": {"cas": "64-17-5", "title": "Absolute Ethanol", "unit": "mL"}} proposals = await real_resolver.import_mapping(data) assert proposals assert proposals[0].resource_key == "ethanol_absolute" async def test_find_candidates_finds_marker_match(client, real_resolver): proposals = await real_resolver.find_candidates("ethanol_absolute") assert any(p.item_id == 12 for p in proposals) async def test_find_candidates_finds_exact_title_match(client, real_resolver): """Exact title match is a setup-time suggestion only — proposals carry scores.""" client.items[14] = make_item(id=14, title="sodium_azide", body="") proposals = await real_resolver.find_candidates("sodium_azide") assert any(p.item_id == 14 for p in proposals) proposal = next(p for p in proposals if p.item_id == 14) assert proposal.score is not None async def test_exact_title_never_applies_silently_at_runtime(client, real_resolver): """An exact title match alone must not create a runtime binding (plan §5.4).""" client.items[14] = make_item(id=14, title="sodium_azide", body="") with pytest.raises(ClarificationNeededError): await real_resolver.resolve("sodium_azide")