| 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 |
|
|