"""Resolver guards against the local dev database (rolled back).""" from __future__ import annotations import uuid from collections.abc import AsyncIterator import pytest from sqlalchemy.ext.asyncio import AsyncConnection from aiatlas import db from aiatlas.db import execute, fetch_one from aiatlas.sdk.facts import EntityRef, Facts from aiatlas.sdk.resolution import Resolver, compatible_types from aiatlas.sdk.writer import FactWriter from aiatlas.services.merge import merge_entities, record_decision @pytest.fixture async def conn() -> AsyncIterator[AsyncConnection]: async with db.engine().connect() as c: trans = await c.begin() try: yield c finally: await trans.rollback() await db.dispose() def _tag() -> str: return uuid.uuid4().hex[:8] async def _write(conn: AsyncConnection, facts: Facts, *, tier: int = 1) -> FactWriter: w = FactWriter(conn, source_id=None, snapshot_id=None, source_url="https://example.com/x", tier=tier, connector_name="test", run_id="run_test", source_key="") await w.write(facts) return w def test_compatible_types() -> None: assert set(compatible_types("model")) == {"model", "artifact"} and compatible_types("paper") == ("paper",) async def test_alias_collision_requires_variant_key(conn: AsyncConnection) -> None: t = _tag() f = Facts() a = f.entity("model", f"Qwen{t}-8B") await _write(conn, f) r = Resolver(conn, source_tier=2) # "Qwen{t} 38B" normalises to the same alias key but is another size → must not resolve onto the 8B model other = await r.resolve(EntityRef("model", f"Qwen{t} 38B"), create=False) assert other is None same = await r.resolve(EntityRef("model", f"Qwen{t} 8B"), create=False) assert same == a.id async def test_keep_separate_decision_blocks_alias_and_merge(conn: AsyncConnection) -> None: t = _tag() f = Facts() org1 = f.entity("company", f"Org One {t}") org2 = f.entity("company", f"Org Two {t}") a = f.entity("model", f"Nimbus {t} Pro", organization=org1, identifiers={"hf_repo": f"one/nimbus-{t}"}, aliases=[f"Nimbus {t}"]) b = f.entity("model", f"Nimbus {t} b", organization=org2, aliases=[f"Nimbus {t}"]) await _write(conn, f) await record_decision(conn, a.id, b.id, "keep_separate", actor="test", note="different vendors") r = Resolver(conn, source_tier=2) # ambiguous alias with both candidates kept separate → no guess; with the organisation of B → B; the pair is never merged automatically assert await r.resolve(EntityRef("model", f"Nimbus {t}"), create=False) is None assert await r.resolve(EntityRef("model", f"Nimbus {t}", organization=EntityRef("company", f"Org One {t}", id=org1.id)), create=False) == a.id assert await r.resolve(EntityRef("model", f"Nimbus {t}", organization=EntityRef("company", f"Org Two {t}", id=org2.id)), create=False) == b.id with pytest.raises(ValueError): await merge_entities(conn, a.id, b.id) async def test_model_ref_resolves_to_artifact_row(conn: AsyncConnection) -> None: t = _tag() f = Facts() art = f.entity("artifact", f"unsloth/Zeta-{t}-GGUF", identifiers={"hf_repo": f"unsloth/Zeta-{t}-GGUF"}) await _write(conn, f) r = Resolver(conn, source_tier=2) assert await r.resolve(EntityRef("model", f"Zeta-{t}-GGUF", identifiers={"hf_repo": f"unsloth/Zeta-{t}-GGUF"}), create=False) == art.id async def test_resolve_variant_only_for_evaluator_refs(conn: AsyncConnection) -> None: t = _tag() f = Facts() org = f.entity("company", f"Zorg {t}") base = f.entity("model", f"Zeta {t}", organization=org, identifiers={"artificial_analysis": f"zeta-{t}"}) await _write(conn, f, tier=2) r = Resolver(conn, source_tier=2) folded = await r.resolve_variant(EntityRef("model", f"zeta-{t}-xhigh")) assert folded == (base.id, {"reasoning_effort": "xhigh"}) assert await r.resolve_variant(EntityRef("model", f"zeta-{t}")) is None # not a variant assert await r.resolve_variant(EntityRef("model", f"omega-{t}-high")) is None # base unknown # a ref with its own hub identifier is a real model even if its name ends in "-thinking": created, never folded real = await r.resolve(EntityRef("model", f"zeta-{t}-thinking", identifiers={"hf_repo": f"zorg/Zeta-{t}-Thinking"})) assert real != base.id # a tier-1 resolver never folds either r1 = Resolver(conn, source_tier=1) official = await r1.resolve(EntityRef("model", f"zeta-{t}-high"), create=False) assert official is None # an evaluator-only ref folds and records the fold r2 = Resolver(conn, source_tier=2) eid = await r2.resolve(EntityRef("model", f"zeta-{t}-high", identifiers={"artificial_analysis": f"zeta-{t}-high"})) assert eid == base.id and r2.folded[f"model:artificial_analysis=zeta-{t}-high"] == (base.id, {"reasoning_effort": "high"}) async def test_first_seen_hint_and_touch(conn: AsyncConnection) -> None: from datetime import UTC, datetime t = _tag() r = Resolver(conn, source_tier=1) eid = await r.resolve(EntityRef("model", f"Zeta {t}", first_seen_hint=datetime(2023, 5, 1, tzinfo=UTC))) row = await fetch_one(conn, "select first_seen_at from entities where id = :id", id=eid) assert row["first_seen_at"] == datetime(2023, 5, 1, tzinfo=UTC) r2 = Resolver(conn, source_tier=1) await r2.resolve(EntityRef("model", f"Zeta {t}", first_seen_hint=datetime(2022, 1, 1, tzinfo=UTC))) row = await fetch_one(conn, "select first_seen_at from entities where id = :id", id=eid) assert row["first_seen_at"] == datetime(2022, 1, 1, tzinfo=UTC) # a future hint never moves first_seen forward await execute(conn, "update entities set first_seen_at = :d where id = :id", d=datetime(2022, 1, 1, tzinfo=UTC), id=eid) await Resolver(conn).resolve(EntityRef("model", f"Zeta {t}", first_seen_hint=datetime(2030, 1, 1, tzinfo=UTC))) row = await fetch_one(conn, "select first_seen_at from entities where id = :id", id=eid) assert row["first_seen_at"] == datetime(2022, 1, 1, tzinfo=UTC)