90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
"""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="<!-- labvoice:resource-key=ethanol_absolute -->")
|
|
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="<!-- labvoice:resource-key=ethanol_absolute -->")
|
|
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")
|