from __future__ import annotations import json from pathlib import Path import pytest from gcmd_classifier.models import ( ArticleClassificationOutcome, ArticleProcessingStatus, ArticleResult, OutputError, ProcessingMetadata, ReviewStatus, RunSummary, ) from gcmd_classifier.persistence import JsonResultStore def _result(doi: str, *, failed: bool = False) -> ArticleResult: if failed: return ArticleResult( DOI=doi, Title="Failed article", Year=2025, Abstract="Text.", processing_status=ArticleProcessingStatus.FAILED, classification_outcome=None, classifications=(), review_status=ReviewStatus.NOT_REQUIRED, errors=(OutputError(code="MODEL_FAILED", message="Model failed."),), processing_metadata=ProcessingMetadata(cache_used=False), ) return ArticleResult( DOI=doi, Title="Completed article", Year=2025, Abstract="Text.", processing_status=ArticleProcessingStatus.COMPLETED, classification_outcome=ArticleClassificationOutcome.NOT_CLASSIFIED, classifications=(), no_classification_reason="No Topic selected.", review_status=ReviewStatus.NOT_REQUIRED, processing_metadata=ProcessingMetadata(cache_used=False), ) def test_article_result_is_saved_after_completion(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) result = _result("10.example/done") store.save_article_result(result) loaded = store.load_article_result("10.example/done") assert loaded == result def test_failed_article_result_is_saved(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) result = _result("10.example/failed", failed=True) store.save_article_result(result) assert store.load_article_result("10.example/failed") == result def test_previous_results_survive_simulated_later_failure(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) first = _result("10.example/first") store.save_article_result(first) with pytest.raises(RuntimeError): raise RuntimeError("later article failed before save") assert store.load_article_result("10.example/first") == first def test_loading_existing_results_works(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) first = _result("10.example/first") second = _result("10.example/second", failed=True) store.save_article_result(first) store.save_article_result(second) loaded = store.load_all_results() assert {result.DOI for result in loaded} == {"10.example/first", "10.example/second"} def test_duplicate_output_records_for_same_doi_are_avoided(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) first = _result("10.example/same") second = _result("10.example/same", failed=True) store.save_article_result(first) store.save_article_result(second) loaded = store.load_all_results() assert len(loaded) == 1 assert loaded[0].processing_status is ArticleProcessingStatus.FAILED def test_consolidated_output_is_valid_json(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) result = _result("10.example/done") summary = RunSummary(run_id="run-1", articles_received=1, articles_completed=1) output_path = store.write_consolidated(results=[result], summary=summary) payload = json.loads(output_path.read_text()) assert payload["summary"]["run_id"] == "run-1" assert payload["articles"][0]["DOI"] == "10.example/done" def test_atomic_checkpoint_temp_file_is_removed_after_success(tmp_path: Path) -> None: store = JsonResultStore(tmp_path) result = _result("10.example/done") store.save_article_result(result) assert store.article_path(result.DOI).exists() assert not list(store.article_path(result.DOI).parent.glob("*.tmp"))