mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-25 23:11:26 -03:00
269 lines
9.6 KiB
Python
269 lines
9.6 KiB
Python
"""Tests for RecipeFTSIndex validation methods."""
|
|
|
|
import os
|
|
import tempfile
|
|
from typing import Any, 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[str, Any]]:
|
|
"""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
|
|
|
|
@staticmethod
|
|
def _forbid_content_scan(monkeypatch):
|
|
"""Fail the test if validation falls back to scanning the FTS content table."""
|
|
|
|
def _raise(*args, **kwargs):
|
|
raise AssertionError("validate_index scanned the FTS content table")
|
|
|
|
monkeypatch.setattr(RecipeFTSIndex, "get_indexed_count", _raise)
|
|
monkeypatch.setattr(RecipeFTSIndex, "get_indexed_recipe_ids", _raise)
|
|
|
|
def test_validate_index_with_metadata_does_not_scan(
|
|
self, temp_db_path, sample_recipes, monkeypatch
|
|
):
|
|
"""Validation must rely on stored metadata, not content-table scans."""
|
|
fts = RecipeFTSIndex(db_path=temp_db_path)
|
|
fts.build_index(sample_recipes)
|
|
|
|
self._forbid_content_scan(monkeypatch)
|
|
|
|
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
|
|
assert result is True
|
|
|
|
# Mismatches are also detected from metadata alone
|
|
result = fts.validate_index(4, {"recipe-001", "recipe-002", "recipe-003"})
|
|
assert result is False
|
|
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-999"})
|
|
assert result is False
|
|
|
|
def test_validate_index_legacy_fallback_writes_metadata(
|
|
self, temp_db_path, sample_recipes, monkeypatch
|
|
):
|
|
"""Indexes built by older versions validate via a one-time scan, then metadata."""
|
|
import sqlite3
|
|
|
|
fts = RecipeFTSIndex(db_path=temp_db_path)
|
|
fts.build_index(sample_recipes)
|
|
|
|
# Simulate a legacy index: drop the fingerprint metadata
|
|
conn = sqlite3.connect(temp_db_path)
|
|
try:
|
|
conn.execute(
|
|
"DELETE FROM fts_metadata WHERE key = ?",
|
|
(RecipeFTSIndex._FINGERPRINT_METADATA_KEY,)
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
# Fallback validation scans once and succeeds
|
|
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
|
|
assert result is True
|
|
|
|
# Metadata was written, so the next validation needs no scan
|
|
self._forbid_content_scan(monkeypatch)
|
|
result = fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"})
|
|
assert result is True
|
|
|
|
def test_validate_index_metadata_tracks_incremental_mutations(
|
|
self, temp_db_path, sample_recipes, monkeypatch
|
|
):
|
|
"""add_recipe/remove_recipe keep the validation metadata in sync."""
|
|
fts = RecipeFTSIndex(db_path=temp_db_path)
|
|
fts.build_index(sample_recipes[:2])
|
|
|
|
fts.add_recipe(sample_recipes[2])
|
|
fts.remove_recipe("recipe-001")
|
|
|
|
self._forbid_content_scan(monkeypatch)
|
|
|
|
result = fts.validate_index(2, {"recipe-002", "recipe-003"})
|
|
assert result is True
|
|
|
|
def test_validate_index_after_clear_uses_metadata(
|
|
self, temp_db_path, sample_recipes, monkeypatch
|
|
):
|
|
"""clear() resets the validation metadata to the empty index state."""
|
|
fts = RecipeFTSIndex(db_path=temp_db_path)
|
|
fts.build_index(sample_recipes)
|
|
fts.clear()
|
|
|
|
self._forbid_content_scan(monkeypatch)
|
|
|
|
assert fts.validate_index(0, set()) is True
|
|
assert fts.validate_index(3, {"recipe-001", "recipe-002", "recipe-003"}) is False
|