Files
ComfyUI-Lora-Manager/tests/test_recipe_fts_index_validation.py
Will Miao eb2af454cc feat: add SQLite-based persistent recipe cache for faster startup
Introduce a new PersistentRecipeCache service that stores recipe metadata in an SQLite database to significantly reduce application startup time. The cache eliminates the need to walk directories and parse JSON files on each launch by persisting recipe data between sessions.

Key features:
- Thread-safe singleton implementation with library-specific instances
- Automatic schema initialization and migration support
- JSON serialization for complex recipe fields (LoRAs, checkpoints, generation parameters, tags)
- File system monitoring with mtime/size validation for cache invalidation
- Environment variable toggle (LORA_MANAGER_DISABLE_PERSISTENT_CACHE) for debugging
- Comprehensive test suite covering save/load cycles, cache invalidation, and edge cases

The cache improves user experience by enabling near-instantaneous recipe loading after the initial cache population, while maintaining data consistency through file change detection.
2026-01-23 22:56:38 +08:00

184 lines
6.4 KiB
Python

"""Tests for RecipeFTSIndex validation methods."""
import os
import tempfile
from typing import Dict, List
import pytest
from py.services.recipe_fts_index import RecipeFTSIndex
@pytest.fixture
def temp_db_path():
"""Create a temporary database path."""
with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f:
path = f.name
yield path
# Cleanup
if os.path.exists(path):
os.unlink(path)
for suffix in ["-wal", "-shm"]:
wal_path = path + suffix
if os.path.exists(wal_path):
os.unlink(wal_path)
@pytest.fixture
def sample_recipes() -> List[Dict]:
"""Create sample recipe data for FTS indexing."""
return [
{
"id": "recipe-001",
"title": "Anime Character Portrait",
"tags": ["anime", "portrait", "character"],
"loras": [
{"file_name": "anime_style", "modelName": "Anime Style LoRA"},
{"file_name": "character_v2", "modelName": "Character Design V2"},
],
"gen_params": {
"prompt": "masterpiece, best quality, 1girl",
"negative_prompt": "bad quality, worst quality",
},
},
{
"id": "recipe-002",
"title": "Landscape Photography",
"tags": ["landscape", "photography", "nature"],
"loras": [
{"file_name": "landscape_lora", "modelName": "Landscape Enhancement"},
],
"gen_params": {
"prompt": "beautiful landscape, mountains, sunset",
"negative_prompt": "ugly, blurry",
},
},
{
"id": "recipe-003",
"title": "Fantasy Art Scene",
"tags": ["fantasy", "art"],
"loras": [],
"gen_params": {
"prompt": "fantasy world, dragons, magic",
},
},
]
class TestFTSIndexValidation:
"""Tests for FTS index validation methods."""
def test_validate_index_empty_returns_false(self, temp_db_path):
"""Test that validation fails on empty index."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.initialize()
# Empty index should not validate against non-empty recipe set
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
assert result is False
def test_validate_index_count_mismatch(self, temp_db_path, sample_recipes):
"""Test validation fails when counts don't match."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
# Validate with wrong count
result = fts.validate_index(5, {"recipe-001", "recipe-002", "recipe-003"})
assert result is False
def test_validate_index_id_mismatch(self, temp_db_path, sample_recipes):
"""Test validation fails when IDs don't match."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
# Validate with wrong IDs
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-999"})
assert result is False
def test_validate_index_success(self, temp_db_path, sample_recipes):
"""Test successful validation."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
# Validate with correct count and IDs
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
assert result is True
def test_get_indexed_recipe_ids(self, temp_db_path, sample_recipes):
"""Test getting indexed recipe IDs."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
ids = fts.get_indexed_recipe_ids()
assert ids == {"recipe-001", "recipe-002", "recipe-003"}
def test_get_indexed_recipe_ids_empty(self, temp_db_path):
"""Test getting IDs from empty index."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.initialize()
ids = fts.get_indexed_recipe_ids()
assert ids == set()
def test_validate_after_add_recipe(self, temp_db_path, sample_recipes):
"""Test validation after adding a recipe."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes[:2]) # Only first 2
# Validation should fail with all 3 IDs
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
assert result is False
# Add third recipe
fts.add_recipe(sample_recipes[2])
# Now validation should pass
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
assert result is True
def test_validate_after_remove_recipe(self, temp_db_path, sample_recipes):
"""Test validation after removing a recipe."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
# Remove a recipe
fts.remove_recipe("recipe-002")
# Validation should fail with original 3 IDs
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
assert result is False
# Validation should pass with 2 remaining IDs
result = fts.validate_index(2, {"recipe-001", "recipe-003"})
assert result is True
def test_validate_index_uninitialized(self, temp_db_path):
"""Test validation on uninitialized index."""
fts = RecipeFTSIndex(db_path=temp_db_path)
# Don't call initialize
# Should initialize automatically and return False for non-empty set
result = fts.validate_index(1, {"recipe-001"})
assert result is False
def test_indexed_count_after_clear(self, temp_db_path, sample_recipes):
"""Test count after clearing index."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
assert fts.get_indexed_count() == 3
fts.clear()
assert fts.get_indexed_count() == 0
def test_search_still_works_after_validation(self, temp_db_path, sample_recipes):
"""Test that search works correctly after validation."""
fts = RecipeFTSIndex(db_path=temp_db_path)
fts.build_index(sample_recipes)
# Validate (which checks state)
fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
# Search should still work
results = fts.search("anime")
assert "recipe-001" in results