Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| from pathlib import Path | |
| from gcmd_classifier.config import ModelSettings | |
| from gcmd_classifier.models import ( | |
| ArticleClassificationOutcome, | |
| ArticleProcessingStatus, | |
| ArticleRecord, | |
| ArticleResult, | |
| ProcessingMetadata, | |
| ReviewStatus, | |
| ) | |
| from gcmd_classifier.persistence import ArticleResultCache, build_cache_identity | |
| from gcmd_classifier.vocabulary import load_vocabulary | |
| FIXTURE_PATH = Path("tests/fixtures/gcmd_hierarchy_small.json") | |
| def _article(**overrides: object) -> ArticleRecord: | |
| values = { | |
| "DOI": "10.example/cache", | |
| "Title": "Carbon dioxide observations", | |
| "Year": 2025, | |
| "Abstract": "Atmospheric carbon dioxide profiles are discussed.", | |
| } | |
| values.update(overrides) | |
| return ArticleRecord.model_validate(values) | |
| def _result(article: ArticleRecord) -> ArticleResult: | |
| return ArticleResult( | |
| DOI=article.DOI, | |
| Title=article.Title, | |
| Year=article.Year, | |
| Abstract=article.Abstract, | |
| 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 _identity(article: ArticleRecord, settings: ModelSettings | None = None, **kwargs): | |
| return build_cache_identity( | |
| article=article, | |
| vocabulary=load_vocabulary(FIXTURE_PATH), | |
| settings=settings or ModelSettings(), | |
| application_version=kwargs.pop("application_version", "0.1.0"), | |
| relevant_config=kwargs.pop("relevant_config", None), | |
| ) | |
| def test_cache_key_includes_article_fingerprint() -> None: | |
| original = _identity(_article()) | |
| changed = _identity(_article(Title="Changed title")) | |
| assert original.article_fingerprint != changed.article_fingerprint | |
| assert original.cache_key != changed.cache_key | |
| def test_cache_key_changes_when_article_content_changes() -> None: | |
| assert ( | |
| _identity(_article(Abstract="A")).cache_key != _identity(_article(Abstract="B")).cache_key | |
| ) | |
| def test_cache_key_changes_when_vocabulary_hash_changes() -> None: | |
| identity = _identity(_article()) | |
| changed = identity.model_copy(update={"vocabulary_version": "different-vocab"}) | |
| assert identity.cache_key != changed.cache_key | |
| def test_cache_key_changes_when_model_provider_name_or_parameters_change() -> None: | |
| default = _identity(_article()) | |
| provider = _identity(_article(), ModelSettings(provider="other")) | |
| name = _identity(_article(), ModelSettings(model_name="other-model")) | |
| temperature = _identity(_article(), ModelSettings(temperature=0.2)) | |
| assert len({default.cache_key, provider.cache_key, name.cache_key, temperature.cache_key}) == 4 | |
| def test_cache_key_changes_when_prompt_versions_change() -> None: | |
| default = _identity(_article()) | |
| changed = _identity(_article(), ModelSettings(prompt_version_topic="topic-v2")) | |
| assert default.cache_key != changed.cache_key | |
| def test_cache_key_changes_when_relevant_config_changes() -> None: | |
| default = _identity(_article(), relevant_config={"candidate_limit": 100}) | |
| changed = _identity(_article(), relevant_config={"candidate_limit": 200}) | |
| assert default.cache_key != changed.cache_key | |
| def test_cache_hit_returns_cache_used_true_and_completed_status(tmp_path: Path) -> None: | |
| article = _article() | |
| identity = _identity(article) | |
| cache = ArticleResultCache(tmp_path) | |
| cache.put(identity, _result(article)) | |
| cached = cache.get(identity) | |
| assert cached is not None | |
| assert cached.processing_metadata.cache_used is True | |
| assert cached.processing_status is ArticleProcessingStatus.COMPLETED | |
| assert cached.processing_status is not ArticleProcessingStatus.SKIPPED | |
| def test_stale_cache_is_not_reused(tmp_path: Path) -> None: | |
| article = _article() | |
| identity = _identity(article) | |
| stale_identity = _identity(_article(Title="Changed title")) | |
| cache = ArticleResultCache(tmp_path) | |
| cache.put(identity, _result(article)) | |
| assert cache.get(stale_identity) is None | |