Files
kenyonxu-ComfyUI_KaiUtilities/tests/test_nodes.py
T
kenyonxu 47193575d6 feat: initial implementation of KaiCivitaiLoRALoader
- Cache manager with JSON read/write, TTL, corruption handling
- Civitai client with SHA256 hash (sync+async) and API fetch
- KaiCivitaiLoRALoader node with trigger words and JSON output
- Node registration, HTTP routes, and secure image serving
- Frontend extension with image preview, pagination, and NSFW blur
- 49 unit tests covering all modules
2026-04-20 23:03:48 +08:00

165 lines
6.5 KiB
Python

import json
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, "/home/kai-remote/github/ComfyUI/.worktrees/kai-civitai-lora")
sys.path.insert(
0, "/home/kai-remote/github/ComfyUI/.worktrees/kai-civitai-lora/custom_nodes"
)
from ComfyUI_KaiUtilities.nodes import KaiCivitaiLoRALoader
class TestKaiCivitaiLoRALoader:
def test_inherits_lora_loader(self):
from nodes import LoraLoader
assert issubclass(KaiCivitaiLoRALoader, LoraLoader)
def test_input_types_has_required_fields(self):
inputs = KaiCivitaiLoRALoader.INPUT_TYPES()
required = inputs.get("required", {})
assert "model" in required
assert "clip" in required
assert "lora_name" in required
assert "strength_model" in required
assert "strength_clip" in required
def test_return_types(self):
assert KaiCivitaiLoRALoader.RETURN_TYPES == ("MODEL", "CLIP", "STRING", "STRING")
def test_return_names(self):
assert KaiCivitaiLoRALoader.RETURN_NAMES == (
"model", "clip", "trigger_words", "civitai_json"
)
def test_function_name(self):
assert KaiCivitaiLoRALoader.FUNCTION == "load_lora_with_info"
def test_load_lora_with_info_returns_empty_when_no_cache(self):
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(return_value=None)
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result == ("model_out", "clip_out", "", "")
def test_load_lora_with_info_returns_trigger_words_and_json(self):
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
cache_data = {
"hash": "abc123",
"name": "Test LoRA",
"triggerWords": ["style", "masterpiece"],
"fetched_at": "2026-04-20T10:00:00Z",
}
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(return_value=cache_data)
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result[0] == "model_out"
assert result[1] == "clip_out"
assert result[2] == "style, masterpiece"
assert json.loads(result[3]) == cache_data
def test_load_lora_with_info_path_error_returns_empty(self):
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
with patch("folder_paths.get_full_path_or_raise", side_effect=FileNotFoundError("not found")):
result = node.load_lora_with_info(
"model", "clip", "missing.safetensors", 1.0, 1.0
)
assert result == ("model_out", "clip_out", "", "")
def test_load_lora_with_info_trained_words_none(self):
"""trainedWords can be None from Civitai API."""
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
cache_data = {
"hash": "abc123",
"name": "Test LoRA",
"triggerWords": None,
"fetched_at": "2026-04-20T10:00:00Z",
}
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(return_value=cache_data)
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result[2] == ""
def test_load_lora_with_info_empty_trigger_words(self):
"""triggerWords can be empty list from Civitai API."""
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
cache_data = {
"hash": "abc123",
"name": "Test LoRA",
"triggerWords": [],
"fetched_at": "2026-04-20T10:00:00Z",
}
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(return_value=cache_data)
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result[2] == ""
def test_load_lora_with_info_nonlist_trigger_words(self):
"""triggerWords can be malformed (non-list)."""
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
cache_data = {
"hash": "abc123",
"name": "Test LoRA",
"triggerWords": "not a list",
"fetched_at": "2026-04-20T10:00:00Z",
}
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(return_value=cache_data)
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result[2] == ""
def test_load_lora_with_info_cache_lookup_error(self):
"""Cache lookup failure should not crash the node."""
node = KaiCivitaiLoRALoader()
node.load_lora = MagicMock(return_value=("model_out", "clip_out"))
with patch("ComfyUI_KaiUtilities.nodes.cache_manager") as mock_cache:
mock_cache.get_cache_for_file_sync = MagicMock(side_effect=RuntimeError("unexpected"))
with patch("folder_paths.get_full_path_or_raise", return_value="/fake/lora.safetensors"):
result = node.load_lora_with_info(
"model", "clip", "test_lora.safetensors", 1.0, 1.0
)
assert result == ("model_out", "clip_out", "", "")