|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import json |
5 | 6 | from typing import TYPE_CHECKING, Any |
6 | 7 |
|
7 | 8 | import pytest |
8 | 9 | from pydantic import BaseModel |
9 | 10 |
|
10 | | -from autointent.generation._cache import StructuredOutputCache |
| 11 | +from autointent.generation._cache import StructuredOutputCache, _get_structured_output_cache_path |
11 | 12 | from autointent.generation.chat_templates import Role |
12 | 13 |
|
13 | 14 | if TYPE_CHECKING: |
@@ -98,3 +99,59 @@ async def test_async_disabled_cache_is_noop() -> None: |
98 | 99 | cache = StructuredOutputCache(use_cache=False) |
99 | 100 | await cache.set_async(MESSAGES, CacheModel, PARAMS, CacheModel(name="a", value=1)) |
100 | 101 | assert await cache.get_async(MESSAGES, CacheModel, PARAMS) is None |
| 102 | + |
| 103 | + |
| 104 | +# --- Regression tests for the on-disk-cache bugs (#326 eager load, #327 eviction) --- |
| 105 | +# Disk entries are directories (PydanticModelDumper writes class_info.json + |
| 106 | +# model_dump.json), so eager load must collect directories and eviction must |
| 107 | +# rmtree rather than unlink. |
| 108 | + |
| 109 | + |
| 110 | +def test_eager_load_populates_memory_from_disk() -> None: |
| 111 | + """A fresh instance eagerly batch-loads existing on-disk entries into memory (#326).""" |
| 112 | + StructuredOutputCache(use_cache=True).set(MESSAGES, CacheModel, PARAMS, CacheModel(name="x", value=9)) |
| 113 | + |
| 114 | + fresh = StructuredOutputCache(use_cache=True) |
| 115 | + key = fresh._get_cache_key(MESSAGES, CacheModel, PARAMS) |
| 116 | + |
| 117 | + # populated at construction by the eager load, before any get() call |
| 118 | + assert key in fresh._memory_cache |
| 119 | + assert isinstance(fresh._memory_cache[key], CacheModel) |
| 120 | + |
| 121 | + |
| 122 | +def test_eager_load_removes_corrupted_entry() -> None: |
| 123 | + """A cache directory whose payload fails to load is skipped and cleaned up, not raised.""" |
| 124 | + entry = _get_structured_output_cache_path("corrupted-entry") |
| 125 | + entry.mkdir(parents=True) |
| 126 | + (entry / "class_info.json").write_text(json.dumps({"name": CacheModel.__name__, "module": CacheModel.__module__})) |
| 127 | + # missing the required "value" field -> ValidationError on load |
| 128 | + (entry / "model_dump.json").write_text(json.dumps({"name": "x"})) |
| 129 | + |
| 130 | + cache = StructuredOutputCache(use_cache=True) # eager load must not raise |
| 131 | + |
| 132 | + assert not cache._memory_cache |
| 133 | + assert not entry.exists() |
| 134 | + |
| 135 | + |
| 136 | +def test_disk_type_mismatch_evicts_entry() -> None: |
| 137 | + """A type-mismatched disk entry is evicted (rmtree) instead of crashing on unlink (#327).""" |
| 138 | + cache = StructuredOutputCache(use_cache=True) |
| 139 | + # plant a CacheModel at the key the cache derives for OtherModel inputs |
| 140 | + key = cache._get_cache_key(MESSAGES, OtherModel, PARAMS) |
| 141 | + cache._save_to_disk(key, CacheModel(name="x", value=1)) |
| 142 | + cache._memory_cache.clear() |
| 143 | + |
| 144 | + assert cache._load_from_disk(key, OtherModel) is None |
| 145 | + assert not _get_structured_output_cache_path(key).exists() |
| 146 | + |
| 147 | + |
| 148 | +@pytest.mark.asyncio |
| 149 | +async def test_async_disk_type_mismatch_evicts_entry() -> None: |
| 150 | + """Async type-mismatched disk entry is evicted (rmtree) instead of crashing on unlink (#327).""" |
| 151 | + cache = StructuredOutputCache(use_cache=True) |
| 152 | + key = cache._get_cache_key(MESSAGES, OtherModel, PARAMS) |
| 153 | + await cache._save_to_disk_async(key, CacheModel(name="x", value=1)) |
| 154 | + cache._memory_cache.clear() |
| 155 | + |
| 156 | + assert await cache._load_from_disk_async(key, OtherModel) is None |
| 157 | + assert not _get_structured_output_cache_path(key).exists() |
0 commit comments