| 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") |
|
|
| |
| |
| |
| 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" |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|
| |
| 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 == [] |
| 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 |
|
|