acl-api / tests /sync /test_run_sync.py
ivykopal's picture
feat: add pdf_url field to paper metadata and update related functionality
cbd62a3
Raw
History Blame Contribute Delete
9.41 kB
import json
import os
import numpy as np
import pytest
from common.db import init_db, get_paper
from common.vector_index import load_index
from sync.run_sync import run_sync
def test_run_sync_embeds_only_new_papers(tmp_path, monkeypatch):
work_dir = str(tmp_path / "work")
monkeypatch.setattr("sync.run_sync.download_snapshot", lambda repo_id, local_dir, token: {"papers": {}, "last_synced_at": None})
uploaded = {}
monkeypatch.setattr("sync.run_sync.upload_snapshot", lambda repo_id, local_dir, token: uploaded.setdefault("called", True))
papers = [
{"id": "p1", "title": "Title One", "abstract": "Abstract one", "authors": "A", "venue": "ACL", "year": 2023, "url": "http://x/p1", "bibtex": "@inproceedings{p1}", "pdf_url": "http://x/p1.pdf"},
]
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter(papers))
embed_calls = []
class FakeClient:
def __init__(self, *a, **kw):
pass
def embed_batch(self, texts):
embed_calls.append(texts)
return [np.ones(1024, dtype=np.float32) for _ in texts]
monkeypatch.setattr("sync.run_sync.EmbeddingsClient", FakeClient)
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
assert embed_calls == [["Title One\n\nAbstract one"]]
assert uploaded.get("called") is True
conn = init_db(os.path.join(work_dir, "papers.db"))
paper = get_paper(conn, "p1")
assert paper["title"] == "Title One"
assert paper["bibtex"] == "@inproceedings{p1}"
assert paper["pdf_url"] == "http://x/p1.pdf"
index = load_index(os.path.join(work_dir, "index.faiss"))
assert index.ntotal == 1
assert paper["faiss_id"] == 0
with open(os.path.join(work_dir, "state.json")) as f:
state = json.load(f)
assert "last_synced_at" in state
assert state["last_synced_at"] is not None
assert "papers" in state
def test_run_sync_raises_and_skips_upload_on_embedding_failure(tmp_path, monkeypatch):
work_dir = str(tmp_path / "work")
monkeypatch.setattr("sync.run_sync.download_snapshot", lambda repo_id, local_dir, token: {"papers": {}, "last_synced_at": None})
uploaded = {}
monkeypatch.setattr("sync.run_sync.upload_snapshot", lambda repo_id, local_dir, token: uploaded.setdefault("called", True))
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter([
{"id": "p1", "title": "T", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": ""},
]))
class FailingClient:
def __init__(self, *a, **kw):
pass
def embed_batch(self, texts):
raise RuntimeError("embedding API down")
monkeypatch.setattr("sync.run_sync.EmbeddingsClient", FailingClient)
with pytest.raises(RuntimeError):
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
assert "called" not in uploaded
def test_run_sync_twice_updates_faiss_id_mapping_on_reembed(tmp_path, monkeypatch):
"""Regression test for Fix #1: re-embedding an existing paper on a second
sync run must supersede its old faiss_id rather than leaving a stale
mapping, since add_vector always appends (never overwrites) in FAISS."""
from common.db import get_paper_id_by_faiss_id
work_dir = str(tmp_path / "work")
# download_snapshot reads the on-disk state.json produced by the previous
# run_sync call, mimicking real behavior across two syncs against the
# same work_dir.
def fake_download_snapshot(repo_id, local_dir, token):
state_path = os.path.join(local_dir, "state.json")
if os.path.exists(state_path):
with open(state_path) as f:
content = f.read().strip()
return json.loads(content) if content else {"papers": {}, "last_synced_at": None}
return {"papers": {}, "last_synced_at": None}
monkeypatch.setattr("sync.run_sync.download_snapshot", fake_download_snapshot)
monkeypatch.setattr("sync.run_sync.upload_snapshot", lambda repo_id, local_dir, token: None)
class FakeClient:
def __init__(self, *a, **kw):
pass
def embed_batch(self, texts):
return [np.ones(1024, dtype=np.float32) for _ in texts]
monkeypatch.setattr("sync.run_sync.EmbeddingsClient", FakeClient)
papers_round1 = [
{"id": "A", "title": "Paper A", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/a"},
{"id": "B", "title": "Paper B", "abstract": "original", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/b"},
]
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter(papers_round1))
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
conn = init_db(os.path.join(work_dir, "papers.db"))
b_old_faiss_id = get_paper(conn, "B")["faiss_id"]
assert get_paper_id_by_faiss_id(conn, b_old_faiss_id) == "B"
# Second sync: B changes content (forcing re-embed/append), C is new.
papers_round2 = [
{"id": "A", "title": "Paper A", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/a"},
{"id": "B", "title": "Paper B", "abstract": "changed abstract", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/b"},
{"id": "C", "title": "Paper C", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/c"},
]
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter(papers_round2))
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
conn = init_db(os.path.join(work_dir, "papers.db"))
b_new_faiss_id = get_paper(conn, "B")["faiss_id"]
assert b_new_faiss_id > b_old_faiss_id
assert get_paper_id_by_faiss_id(conn, b_old_faiss_id) is None
assert get_paper_id_by_faiss_id(conn, b_new_faiss_id) == "B"
def test_run_sync_backfills_bibtex_without_reembedding(tmp_path, monkeypatch):
"""Bibtex and pdf_url must reach unchanged papers via the decoupled
metadata write, not via re-embedding. Round 1 simulates pre-feature data
(no bibtex/pdf_url keys -> stored as ''); round 2 supplies the same
paper, now with both fields, but identical content hash -> unchanged ->
zero embed calls, yet both fields end up populated. This is the
guarantee that adding these fields does NOT trigger a full re-embed
(which would orphan FAISS vectors, since add_vector only appends)."""
work_dir = str(tmp_path / "work")
def fake_download_snapshot(repo_id, local_dir, token):
state_path = os.path.join(local_dir, "state.json")
if os.path.exists(state_path):
with open(state_path) as f:
content = f.read().strip()
return json.loads(content) if content else {"papers": {}, "last_synced_at": None}
return {"papers": {}, "last_synced_at": None}
monkeypatch.setattr("sync.run_sync.download_snapshot", fake_download_snapshot)
monkeypatch.setattr("sync.run_sync.upload_snapshot", lambda repo_id, local_dir, token: None)
embed_calls = []
class FakeClient:
def __init__(self, *a, **kw):
pass
def embed_batch(self, texts):
embed_calls.append(list(texts))
return [np.ones(1024, dtype=np.float32) for _ in texts]
monkeypatch.setattr("sync.run_sync.EmbeddingsClient", FakeClient)
# Round 1: legacy paper without a bibtex key (stored as '').
papers_round1 = [
{"id": "A", "title": "Paper A", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/a"},
]
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter(papers_round1))
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
conn = init_db(os.path.join(work_dir, "papers.db"))
assert get_paper(conn, "A")["bibtex"] == ""
assert get_paper(conn, "A")["pdf_url"] == ""
embed_calls.clear()
# Round 2: same content (unchanged -> not re-embedded) but now with bibtex/pdf_url.
papers_round2 = [
{"id": "A", "title": "Paper A", "abstract": "", "authors": "", "venue": "ACL", "year": 2023, "url": "http://x/a", "bibtex": "@inproceedings{a}", "pdf_url": "http://x/a.pdf"},
]
monkeypatch.setattr("sync.run_sync.iter_papers", lambda path: iter(papers_round2))
run_sync(
anthology_path="/fake", hf_repo_id="org/repo", hf_token="tok",
embedding_base_url="http://fake", embedding_api_key="key", work_dir=work_dir,
)
assert embed_calls == [] # no re-embedding of an unchanged paper
conn = init_db(os.path.join(work_dir, "papers.db"))
paper = get_paper(conn, "A")
assert paper["bibtex"] == "@inproceedings{a}"
assert paper["pdf_url"] == "http://x/a.pdf"
index = load_index(os.path.join(work_dir, "index.faiss"))
assert index.ntotal == 1 # no orphaned second vector