Files
ComfyUI-Lora-Manager/tests/nodes/test_lora_randomizer.py
Will Miao 6a17e75782 docs: add frontend UI architecture and ComfyUI widget guidelines
- Document dual UI systems: standalone web UI and ComfyUI custom node widgets
- Add ComfyUI widget development guidelines including styling and constraints
- Update terminology in LoraRandomizerNode from 'frontend/backend' to 'fixed/always' for clarity
- Include UI constraints for ComfyUI widgets: minimize vertical space, avoid dynamic height changes, keep UI simple
2026-01-13 11:20:50 +08:00

322 lines
9.0 KiB
Python

"""Tests for LoraRandomizerNode roll_mode functionality"""
from unittest.mock import AsyncMock
import pytest
from py.nodes.lora_randomizer import LoraRandomizerNode
from py.services import service_registry
@pytest.fixture
def randomizer_node():
"""Create a LoraRandomizerNode instance for testing"""
return LoraRandomizerNode()
@pytest.fixture
def sample_loras():
"""Sample loras input"""
return [
{
"name": "lora1.safetensors",
"strength": 0.8,
"clipStrength": 0.8,
"active": True,
"expanded": False,
"locked": True,
},
{
"name": "lora2.safetensors",
"strength": 0.6,
"clipStrength": 0.6,
"active": True,
"expanded": False,
"locked": False,
},
]
@pytest.fixture
def randomizer_config_fixed():
"""Randomizer config with roll_mode='fixed'"""
return {
"count_mode": "fixed",
"count_fixed": 3,
"count_min": 2,
"count_max": 5,
"model_strength_min": 0.5,
"model_strength_max": 1.0,
"use_same_clip_strength": True,
"clip_strength_min": 0.5,
"clip_strength_max": 1.0,
"roll_mode": "fixed",
}
@pytest.fixture
def randomizer_config_always():
"""Randomizer config with roll_mode='always'"""
return {
"count_mode": "fixed",
"count_fixed": 3,
"count_min": 2,
"count_max": 5,
"model_strength_min": 0.5,
"model_strength_max": 1.0,
"use_same_clip_strength": True,
"clip_strength_min": 0.5,
"clip_strength_max": 1.0,
"roll_mode": "always",
}
@pytest.mark.asyncio
async def test_roll_mode_fixed_returns_input_loras(
randomizer_node, sample_loras, randomizer_config_fixed, mock_scanner, monkeypatch
):
"""Test that fixed mode returns input loras as ui_loras"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "new_lora.safetensors",
"file_path": "/path/to/new_lora.safetensors",
"folder": "",
}
]
result = await randomizer_node.randomize(
randomizer_config_fixed, sample_loras, pool_config=None
)
assert "result" in result
assert "ui" in result
assert "loras" in result["ui"]
assert "last_used" in result["ui"]
ui_loras = result["ui"]["loras"]
last_used = result["ui"]["last_used"]
assert ui_loras == sample_loras
assert last_used == sample_loras
@pytest.mark.asyncio
async def test_roll_mode_always_generates_new_loras(
randomizer_node, sample_loras, randomizer_config_always, mock_scanner, monkeypatch
):
"""Test that always mode generates new random loras"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "random_lora1.safetensors",
"file_path": "/path/to/random_lora1.safetensors",
"folder": "",
},
{
"file_name": "random_lora2.safetensors",
"file_path": "/path/to/random_lora2.safetensors",
"folder": "",
},
]
result = await randomizer_node.randomize(
randomizer_config_always, sample_loras, pool_config=None
)
ui_loras = result["ui"]["loras"]
last_used = result["ui"]["last_used"]
assert last_used == sample_loras
assert ui_loras != sample_loras
@pytest.mark.asyncio
async def test_roll_mode_always_preserves_locked_loras(
randomizer_node, sample_loras, randomizer_config_always, mock_scanner, monkeypatch
):
"""Test that always mode preserves locked loras from input"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "random_lora.safetensors",
"file_path": "/path/to/random_lora.safetensors",
"folder": "",
}
]
result = await randomizer_node.randomize(
randomizer_config_always, sample_loras, pool_config=None
)
ui_loras = result["ui"]["loras"]
locked_lora = next((l for l in ui_loras if l.get("locked")), None)
assert locked_lora is not None
assert locked_lora["name"] == "lora1.safetensors"
assert locked_lora["strength"] == 0.8
@pytest.mark.asyncio
async def test_last_used_always_input_loras(
randomizer_node, sample_loras, randomizer_config_fixed, mock_scanner, monkeypatch
):
"""Test that last_used is always set to input loras"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "new_lora.safetensors",
"file_path": "/path/to/new_lora.safetensors",
"folder": "",
}
]
result = await randomizer_node.randomize(
randomizer_config_fixed, sample_loras, pool_config=None
)
last_used = result["ui"]["last_used"]
assert last_used == sample_loras
@pytest.mark.asyncio
async def test_execution_stack_built_from_input_loras(
randomizer_node, sample_loras, randomizer_config_fixed, mock_scanner, monkeypatch
):
"""Test that execution stack is always built from input loras (current user selection)"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "lora1.safetensors",
"file_path": "/path/to/lora1.safetensors",
"folder": "",
},
{
"file_name": "lora2.safetensors",
"file_path": "/path/to/lora2.safetensors",
"folder": "",
},
]
result = await randomizer_node.randomize(
randomizer_config_fixed, sample_loras, pool_config=None
)
execution_stack = result["result"][0]
ui_loras = result["ui"]["loras"]
# execution_stack should be built from input loras (sample_loras)
assert len(execution_stack) == 2
assert execution_stack[0][1] == 0.8
assert execution_stack[0][2] == 0.8
assert execution_stack[1][1] == 0.6
assert execution_stack[1][2] == 0.6
# ui_loras matches input loras in fixed mode
assert ui_loras == sample_loras
@pytest.mark.asyncio
async def test_roll_mode_default_always(
randomizer_node, sample_loras, mock_scanner, monkeypatch
):
"""Test that default roll_mode is 'always'"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
config_without_roll_mode = {
"count_mode": "fixed",
"count_fixed": 3,
}
mock_scanner._cache.raw_data = [
{
"file_name": "random_lora.safetensors",
"file_path": "/path/to/random_lora.safetensors",
"folder": "",
}
]
result = await randomizer_node.randomize(
config_without_roll_mode, sample_loras, pool_config=None
)
ui_loras = result["ui"]["loras"]
last_used = result["ui"]["last_used"]
execution_stack = result["result"][0]
# last_used should always be input loras
assert last_used == sample_loras
# ui_loras should be different (new random loras generated)
assert ui_loras != sample_loras
# execution_stack should be built from input loras, not ui_loras
assert len(execution_stack) == 2
assert execution_stack[0][1] == 0.8
assert execution_stack[0][2] == 0.8
assert execution_stack[1][1] == 0.6
assert execution_stack[1][2] == 0.6
@pytest.mark.asyncio
async def test_execution_stack_always_from_input_loras_not_ui_loras(
randomizer_node, sample_loras, randomizer_config_always, mock_scanner, monkeypatch
):
"""Test that execution_stack is always built from input loras, even when ui_loras is different"""
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_lora_scanner",
AsyncMock(return_value=mock_scanner),
)
mock_scanner._cache.raw_data = [
{
"file_name": "new_random_lora.safetensors",
"file_path": "/path/to/new_random_lora.safetensors",
"folder": "",
}
]
result = await randomizer_node.randomize(
randomizer_config_always, sample_loras, pool_config=None
)
execution_stack = result["result"][0]
ui_loras = result["ui"]["loras"]
# ui_loras should be new random loras
assert ui_loras != sample_loras
# execution_stack should be built from input loras (sample_loras), not ui_loras
assert len(execution_stack) == 2
assert execution_stack[0][1] == 0.8
assert execution_stack[0][2] == 0.8
assert execution_stack[1][1] == 0.6
assert execution_stack[1][2] == 0.6