Files
2026-08-30 20:44:00 +02:00

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")