import json from pathlib import Path from zotero_kb.cards import CardBuilder from zotero_kb.config import AppConfig from zotero_kb.projects import ProjectService from zotero_kb.workspace import Workspace from zotero_kb.zotero_reader import ZoteroItemRecord class FakeLlmClient: def generate_card(self, source_bundle: dict[str, object]) -> dict[str, object]: title = str(source_bundle["title"]) if "Scoped" in title: summary = "Project-scoped retrieval improves drafting." claims = ["Project scoping improves citation precision."] else: summary = "Unrelated retrieval baseline." claims = ["Baseline retrieval is broad."] return { "summary": summary, "core_claims": claims, "methods": ["Method details."], "evidence": ["Evidence details."], "quotable_passages": claims, "writing_hints": [summary], "keywords": ["retrieval"], } def _build_item(item_key: str, title: str) -> ZoteroItemRecord: return ZoteroItemRecord( item_key=item_key, title=title, creators=["Alice Smith"], year="2024", item_type="journalArticle", abstract=title, tags=["retrieval"], collection_paths=[["Theory", "Drafting"]], notes=[title], attachment_texts=[title], ) def test_add_item_to_project_updates_selected_items_and_project_index(tmp_path: Path) -> None: config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero") workspace = Workspace(config) workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini") builder = CardBuilder(config.workspace_dir, FakeLlmClient()) builder.build_or_update(_build_item("PAPER0001", "Scoped Retrieval for Drafting")) service = ProjectService(config.workspace_dir) payload = service.add_items("thesis-ch2", ["PAPER0001"]) assert payload["selected_items"] == ["PAPER0001"] assert payload["cards"][0]["item_key"] == "PAPER0001" selected_items = json.loads( (config.workspace_dir / "projects" / "thesis-ch2" / "selected-items.json").read_text(encoding="utf-8") ) assert selected_items == ["PAPER0001"] def test_remove_item_from_project_updates_selected_items(tmp_path: Path) -> None: config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero") workspace = Workspace(config) workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini") builder = CardBuilder(config.workspace_dir, FakeLlmClient()) builder.build_or_update(_build_item("PAPER0001", "Scoped Retrieval for Drafting")) service = ProjectService(config.workspace_dir) service.add_items("thesis-ch2", ["PAPER0001"]) payload = service.remove_item("thesis-ch2", "PAPER0001") assert payload["selected_items"] == [] def test_add_item_to_project_returns_pending_item_when_card_not_generated(tmp_path: Path) -> None: config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero") workspace = Workspace(config) workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini") items_index_path = config.workspace_dir / "library" / "index" / "items.json" items_index_path.parent.mkdir(parents=True, exist_ok=True) items_index_path.write_text( json.dumps( { "PAPER0001": { "item_key": "PAPER0001", "title": "Pending Item", "creators": ["Alice Smith"], "year": "2024", "item_type": "journalArticle", } }, ensure_ascii=False, ), encoding="utf-8", ) service = ProjectService(config.workspace_dir) payload = service.add_items("thesis-ch2", ["PAPER0001"]) assert payload["selected_items"] == ["PAPER0001"] assert len(payload["items"]) == 1 assert payload["items"][0]["item_key"] == "PAPER0001" assert payload["items"][0]["card_status"] == "pending"