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"]) language = str(source_bundle.get("card_language", "en")) if "Scoped" in title: summary = "项目范围检索能改善写作。" if language == "zh" else "Project-scoped retrieval improves drafting." claims = ["项目范围限定能提升引文精度。"] if language == "zh" else ["Project scoping improves citation precision."] else: summary = "无关检索基线。" if language == "zh" else "Unrelated retrieval baseline." claims = ["基线检索范围较宽。"] if language == "zh" else ["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], attachments=[], 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", "en") 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", "en") 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", "zh") 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" def test_project_view_treats_legacy_cards_as_english_only(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", "zh") cards_index_path = config.workspace_dir / "library" / "index" / "cards.json" cards_index_path.parent.mkdir(parents=True, exist_ok=True) cards_index_path.write_text( json.dumps( { "PAPER0001": { "item_key": "PAPER0001", "title": "Legacy Card", "summary": "English legacy summary", "claims": ["Legacy claim"], "citations": [], "quotable_spans": [], "writing_hints": [], "attachments": [], } }, ensure_ascii=False, ), encoding="utf-8", ) items_index_path = config.workspace_dir / "library" / "index" / "items.json" items_index_path.write_text( json.dumps( { "PAPER0001": { "item_key": "PAPER0001", "title": "Legacy Card", "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["items"][0]["card_status"] == "pending" workspace.rename_project("thesis-ch2", "Thesis Chapter 2", "en") english_view = service.get_project_view("thesis-ch2") assert english_view["items"][0]["card_status"] == "done" assert english_view["items"][0]["summary"] == "English legacy summary" def test_project_view_reads_only_current_language_card_variant(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", "zh") cards_index_path = config.workspace_dir / "library" / "index" / "cards.json" cards_index_path.parent.mkdir(parents=True, exist_ok=True) cards_index_path.write_text( json.dumps( { "PAPER0001": { "zh": { "item_key": "PAPER0001", "language": "zh", "title": "Bilingual Card", "summary": "中文摘要", "claims": ["中文 claim"], "citations": [], "quotable_spans": [], "writing_hints": [], "attachments": [], }, "en": { "item_key": "PAPER0001", "language": "en", "title": "Bilingual Card", "summary": "English summary", "claims": ["English claim"], "citations": [], "quotable_spans": [], "writing_hints": [], "attachments": [], }, } }, ensure_ascii=False, ), encoding="utf-8", ) items_index_path = config.workspace_dir / "library" / "index" / "items.json" items_index_path.write_text( json.dumps( { "PAPER0001": { "item_key": "PAPER0001", "title": "Bilingual Card", "creators": ["Alice Smith"], "year": "2024", "item_type": "journalArticle", } }, ensure_ascii=False, ), encoding="utf-8", ) service = ProjectService(config.workspace_dir) zh_view = service.add_items("thesis-ch2", ["PAPER0001"]) assert zh_view["items"][0]["summary"] == "中文摘要" assert zh_view["cards"][0]["language"] == "zh" workspace.rename_project("thesis-ch2", "Thesis Chapter 2", "en") en_view = service.get_project_view("thesis-ch2") assert en_view["items"][0]["summary"] == "English summary" assert en_view["cards"][0]["language"] == "en"