mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
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.
184 lines
6.4 KiB
Python
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
|