Files
Gin3601-LLS-node/tests/test_qwen_nodes.py
T
2026-05-20 17:50:28 +08:00

891 lines
33 KiB
Python

import importlib.util
import pathlib
import sys
import unittest
from unittest import mock
ROOT = pathlib.Path(__file__).resolve().parents[1]
def load_plugin_package():
for name in list(sys.modules):
if name == "lls_node_test_qwen" or name.startswith("lls_node_test_qwen."):
sys.modules.pop(name)
spec = importlib.util.spec_from_file_location(
"lls_node_test_qwen",
ROOT / "__init__.py",
submodule_search_locations=[str(ROOT)],
)
module = importlib.util.module_from_spec(spec)
sys.modules["lls_node_test_qwen"] = module
spec.loader.exec_module(module)
return module
class NodeOutputStub:
def __init__(self, *args):
self.args = args
@property
def result(self):
return self.args
def __getitem__(self, index):
return self.args[index]
class QwenFolderPathsStub:
def __init__(self):
self._files = {
"diffusion_models": [
"qwen_image_fp8_e4m3fn.safetensors",
"qwen_image_2512_fp8_e4m3fn.safetensors",
"qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"qwen_image_edit_2511_bf16.safetensors",
"qwen_image_layered_bf16.safetensors",
"flux1-dev.safetensors",
],
"text_encoders": ["qwen_2.5_vl_7b_fp8_scaled.safetensors"],
"vae": ["qwen_image_vae.safetensors"],
"loras": [
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
"Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
"Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
"flux-dev-style.safetensors",
],
}
def get_filename_list(self, category):
return list(self._files.get(category, []))
class QwenClipStub:
def __init__(self, clip_name, clip_type):
self.clip_name = clip_name
self.clip_type = clip_type
class QwenVAEStub:
def __init__(self, vae_name):
self.vae_name = vae_name
class CoreQwenNodesStub:
last_unet_call = None
last_clip_call = None
last_text_encode_calls = []
last_ksampler_call = None
last_vae_encode_call = None
last_vae_decode_call = None
last_vae_load_call = None
last_lora_calls = []
@classmethod
def reset(cls):
cls.last_unet_call = None
cls.last_clip_call = None
cls.last_text_encode_calls = []
cls.last_ksampler_call = None
cls.last_vae_encode_call = None
cls.last_vae_decode_call = None
cls.last_vae_load_call = None
cls.last_lora_calls = []
class UNETLoader:
def load_unet(self, unet_name, weight_dtype):
CoreQwenNodesStub.last_unet_call = {
"unet_name": unet_name,
"weight_dtype": weight_dtype,
}
return (f"MODEL::{unet_name}",)
class CLIPLoader:
def load_clip(self, clip_name, type="stable_diffusion", device="default"):
CoreQwenNodesStub.last_clip_call = {
"clip_name": clip_name,
"type": type,
"device": device,
}
return (QwenClipStub(clip_name, type),)
class VAELoader:
def load_vae(self, vae_name):
CoreQwenNodesStub.last_vae_load_call = {"vae_name": vae_name}
return (QwenVAEStub(vae_name),)
class CLIPTextEncode:
def encode(self, clip, text):
CoreQwenNodesStub.last_text_encode_calls.append(
{
"clip_name": clip.clip_name,
"clip_type": clip.clip_type,
"text": text,
}
)
return (f"COND::{text}",)
class LoraLoaderModelOnly:
def load_lora_model_only(self, model, lora_name, strength_model):
CoreQwenNodesStub.last_lora_calls.append(
{
"model": model,
"lora_name": lora_name,
"strength_model": strength_model,
}
)
return (f"LORA::{model}::{lora_name}::{strength_model}",)
class KSampler:
def sample(
self,
model,
seed,
steps,
cfg,
sampler_name,
scheduler,
positive,
negative,
latent_image,
denoise=1.0,
):
CoreQwenNodesStub.last_ksampler_call = {
"model": model,
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"positive": positive,
"negative": negative,
"latent_image": latent_image,
"denoise": denoise,
}
return ({"samples": f"SAMPLED::{seed}::{steps}"},)
class VAEDecode:
def decode(self, vae, samples):
CoreQwenNodesStub.last_vae_decode_call = {
"vae_name": vae.vae_name,
"samples": samples,
}
return (f"IMAGE::{samples['samples']}",)
class VAEEncode:
def encode(self, vae, pixels):
CoreQwenNodesStub.last_vae_encode_call = {
"vae_name": vae.vae_name,
"pixels": pixels,
}
return ({"samples": f"ENCODED::{pixels}"},)
class SD3NodesStub:
last_generate_call = None
@classmethod
def reset(cls):
cls.last_generate_call = None
class EmptySD3LatentImage:
def generate(self, width, height, batch_size=1):
SD3NodesStub.last_generate_call = {
"width": width,
"height": height,
"batch_size": batch_size,
}
return (
{
"samples": f"EMPTY::{width}x{height}::{batch_size}",
"downscale_ratio_spacial": 8,
},
)
class ModelAdvancedStub:
last_call = None
@classmethod
def reset(cls):
cls.last_call = None
class ModelSamplingAuraFlow:
def patch_aura(self, model, shift):
ModelAdvancedStub.last_call = {"model": model, "shift": shift}
return (f"AURA::{model}::{shift}",)
class FluxNodesStub:
last_scale_call = None
last_reference_calls = []
@classmethod
def reset(cls):
cls.last_scale_call = None
cls.last_reference_calls = []
class FluxKontextImageScale:
@classmethod
def execute(cls, image):
FluxNodesStub.last_scale_call = {"image": image}
return NodeOutputStub(f"SCALED::{image}")
class FluxKontextMultiReferenceLatentMethod:
@classmethod
def execute(cls, conditioning, reference_latents_method):
FluxNodesStub.last_reference_calls.append(
{
"conditioning": conditioning,
"reference_latents_method": reference_latents_method,
}
)
return NodeOutputStub(f"{conditioning}::{reference_latents_method}")
class QwenExtraNodesStub:
last_calls = []
@classmethod
def reset(cls):
cls.last_calls = []
class TextEncodeQwenImageEditPlus:
@classmethod
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None):
QwenExtraNodesStub.last_calls.append(
{
"clip_name": clip.clip_name,
"prompt": prompt,
"vae_name": getattr(vae, "vae_name", None),
"image1": image1,
"image2": image2,
"image3": image3,
}
)
return NodeOutputStub(f"EDIT_COND::{prompt}::{image1}::{image2}::{image3}")
class CFGNodesStub:
last_call = None
@classmethod
def reset(cls):
cls.last_call = None
class CFGNorm:
@classmethod
def execute(cls, model, strength):
CFGNodesStub.last_call = {"model": model, "strength": strength}
return NodeOutputStub(f"CFGNORM::{model}::{strength}")
class TestQwenNodes(unittest.TestCase):
def setUp(self):
CoreQwenNodesStub.reset()
SD3NodesStub.reset()
ModelAdvancedStub.reset()
FluxNodesStub.reset()
QwenExtraNodesStub.reset()
CFGNodesStub.reset()
def test_plugin_registers_qwen_nodes(self):
plugin = load_plugin_package()
self.assertIn("LLSQwenTextToImage", plugin.NODE_CLASS_MAPPINGS)
self.assertIn("LLSQwenImageEdit", plugin.NODE_CLASS_MAPPINGS)
self.assertNotIn("LLSQwenLoRAStack", plugin.NODE_CLASS_MAPPINGS)
def test_qwen_text_node_filters_only_text_models(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import nodes as qwen_nodes
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
choices = qwen_nodes.LLSQwenTextToImage.INPUT_TYPES()["required"]["model_name"][0]
self.assertEqual(
choices,
[
"qwen_image_2512_fp8_e4m3fn.safetensors",
"qwen_image_fp8_e4m3fn.safetensors",
],
)
def test_qwen_edit_node_filters_only_edit_models(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import nodes as qwen_nodes
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
choices = qwen_nodes.LLSQwenImageEdit.INPUT_TYPES()["required"]["model_name"][0]
self.assertEqual(
choices,
[
"qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"qwen_image_edit_2511_bf16.safetensors",
],
)
def test_qwen_text_node_schema_exposes_advanced_inputs_and_optional_model(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSQwenTextToImage"]
required = node_cls.INPUT_TYPES()["required"]
optional = node_cls.INPUT_TYPES()["optional"]
self.assertEqual(node_cls.RETURN_TYPES, ("IMAGE",))
self.assertEqual(node_cls.RETURN_NAMES, ("image",))
self.assertEqual(
tuple(required.keys()),
(
"model_name",
"prompt",
"width",
"height",
"steps",
"seed",
"batch_size",
"negative_prompt",
"cfg",
"sampler_name",
"scheduler",
"shift",
"enable_turbo_mode",
"turbo_lora_name",
"turbo_strength",
),
)
self.assertEqual(required["negative_prompt"][1]["default"], "")
self.assertEqual(required["cfg"][1]["default"], 4.0)
self.assertIn("euler", required["sampler_name"][0])
self.assertIn("simple", required["scheduler"][0])
self.assertEqual(required["shift"][1]["default"], 3.1)
self.assertEqual(required["enable_turbo_mode"][1]["default"], False)
self.assertEqual(optional["model"][0], "MODEL")
def test_qwen_edit_node_schema_exposes_advanced_inputs_optional_images_and_model(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSQwenImageEdit"]
required = node_cls.INPUT_TYPES()["required"]
optional = node_cls.INPUT_TYPES()["optional"]
self.assertEqual(node_cls.RETURN_TYPES, ("IMAGE",))
self.assertEqual(node_cls.RETURN_NAMES, ("image",))
self.assertEqual(
tuple(required.keys()),
(
"model_name",
"image",
"prompt",
"steps",
"seed",
"negative_prompt",
"cfg",
"sampler_name",
"scheduler",
"shift",
"cfg_norm_strength",
"reference_latents_method",
"enable_turbo_mode",
"turbo_lora_name",
"turbo_strength",
),
)
self.assertEqual(optional["image2"][0], "IMAGE")
self.assertEqual(optional["image3"][0], "IMAGE")
self.assertEqual(optional["model"][0], "MODEL")
self.assertEqual(
required["reference_latents_method"][0],
["offset", "index", "uxo/uno", "index_timestep_zero"],
)
def test_qwen_text_node_turbo_lora_choices_filter_only_text_loras(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import nodes as qwen_nodes
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
choices = qwen_nodes.LLSQwenTextToImage.INPUT_TYPES()["required"]["turbo_lora_name"][0]
self.assertEqual(
choices,
[
"(auto)",
"Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
],
)
def test_qwen_edit_node_turbo_lora_choices_filter_only_edit_loras(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import nodes as qwen_nodes
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
choices = qwen_nodes.LLSQwenImageEdit.INPUT_TYPES()["required"]["turbo_lora_name"][0]
self.assertEqual(
choices,
[
"(auto)",
"Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
],
)
def test_qwen_text_runtime_rejects_incompatible_model_name(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
):
with self.assertRaisesRegex(RuntimeError, "not compatible with LLSQwenTextToImage"):
qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_edit_2511_bf16.safetensors",
prompt="a cat",
negative_prompt="",
width=1024,
height=1024,
steps=20,
seed=1,
batch_size=1,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model=None,
)
def test_qwen_edit_runtime_rejects_incompatible_model_name(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
):
with self.assertRaisesRegex(RuntimeError, "not compatible with LLSQwenImageEdit"):
qwen_runtime.run_qwen_image_edit(
model_name="qwen_image_fp8_e4m3fn.safetensors",
image="IMAGE::input",
image2=None,
image3=None,
prompt="edit the cat",
negative_prompt="",
steps=20,
seed=1,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
cfg_norm_strength=1.0,
reference_latents_method="index_timestep_zero",
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model=None,
)
def test_qwen_text_runtime_raises_when_qwen_text_encoder_is_missing(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
stub._files["text_encoders"] = []
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
):
with self.assertRaisesRegex(RuntimeError, "Missing Qwen text encoder"):
qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="",
width=1024,
height=1024,
steps=20,
seed=1,
batch_size=1,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model=None,
)
def test_qwen_text_runtime_raises_when_qwen_vae_is_missing(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
stub._files["vae"] = []
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
):
with self.assertRaisesRegex(RuntimeError, "Missing Qwen VAE"):
qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="",
width=1024,
height=1024,
steps=20,
seed=1,
batch_size=1,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model=None,
)
def test_qwen_text_runtime_executes_internal_pipeline_when_model_not_connected(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
qwen_runtime,
"nodes_sd3",
SD3NodesStub,
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
image = qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="low quality",
width=1024,
height=1152,
steps=33,
seed=99,
batch_size=2,
cfg=7.5,
sampler_name="heun",
scheduler="normal",
shift=2.25,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model=None,
)
self.assertEqual(image, "IMAGE::SAMPLED::99::33")
self.assertEqual(CoreQwenNodesStub.last_unet_call["unet_name"], "qwen_image_fp8_e4m3fn.safetensors")
self.assertEqual(CoreQwenNodesStub.last_text_encode_calls[0]["text"], "a cat")
self.assertEqual(CoreQwenNodesStub.last_text_encode_calls[1]["text"], "low quality")
self.assertEqual(SD3NodesStub.last_generate_call, {"width": 1024, "height": 1152, "batch_size": 2})
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 33)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 7.5)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["sampler_name"], "heun")
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["scheduler"], "normal")
self.assertEqual(ModelAdvancedStub.last_call["shift"], 2.25)
def test_qwen_text_runtime_uses_external_model_without_internal_unet_load(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
qwen_runtime,
"nodes_sd3",
SD3NodesStub,
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
image = qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="low quality",
width=1024,
height=1024,
steps=20,
seed=5,
batch_size=1,
cfg=4.5,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model="MODEL::external",
)
self.assertEqual(image, "IMAGE::SAMPLED::5::20")
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 4.5)
self.assertEqual(ModelAdvancedStub.last_call["model"], "MODEL::external")
def test_qwen_text_runtime_applies_turbo_lora_on_external_model(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
qwen_runtime,
"nodes_sd3",
SD3NodesStub,
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="",
width=1024,
height=1024,
steps=50,
seed=99,
batch_size=1,
cfg=8.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=True,
turbo_lora_name="(auto)",
turbo_strength=0.75,
model="MODEL::external",
)
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
self.assertEqual(CoreQwenNodesStub.last_lora_calls[0]["model"], "MODEL::external")
self.assertEqual(
CoreQwenNodesStub.last_lora_calls[0]["lora_name"],
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
)
self.assertEqual(CoreQwenNodesStub.last_lora_calls[0]["strength_model"], 0.75)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 8)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 1.0)
def test_qwen_text_runtime_rejects_incompatible_manual_turbo_lora(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
):
with self.assertRaisesRegex(RuntimeError, "Turbo LoRA .* is not compatible"):
qwen_runtime.run_qwen_text_to_image(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
negative_prompt="",
width=1024,
height=1024,
steps=20,
seed=1,
batch_size=1,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=True,
turbo_lora_name="Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
turbo_strength=1.0,
model=None,
)
def test_qwen_edit_runtime_uses_external_model_without_internal_unet_load(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
qwen_runtime,
"nodes_model_advanced",
ModelAdvancedStub,
), mock.patch.object(qwen_runtime, "nodes_qwen", QwenExtraNodesStub), mock.patch.object(
qwen_runtime,
"nodes_flux",
FluxNodesStub,
), mock.patch.object(qwen_runtime, "nodes_cfg", CFGNodesStub):
image = qwen_runtime.run_qwen_image_edit(
model_name="qwen_image_edit_2509_fp8_e4m3fn.safetensors",
image="IMAGE::input",
image2="IMAGE::ref2",
image3="IMAGE::ref3",
prompt="turn the cat blue",
negative_prompt="low quality",
steps=28,
seed=123,
cfg=6.5,
sampler_name="heun",
scheduler="normal",
shift=2.0,
cfg_norm_strength=0.8,
reference_latents_method="index",
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model="MODEL::external-edit",
)
self.assertEqual(image, "IMAGE::SAMPLED::123::28")
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
self.assertEqual(FluxNodesStub.last_scale_call, {"image": "IMAGE::input"})
self.assertEqual(QwenExtraNodesStub.last_calls[0]["prompt"], "turn the cat blue")
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image1"], "SCALED::IMAGE::input")
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image2"], "IMAGE::ref2")
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image3"], "IMAGE::ref3")
self.assertEqual(QwenExtraNodesStub.last_calls[1]["prompt"], "low quality")
self.assertEqual(FluxNodesStub.last_reference_calls[0]["reference_latents_method"], "index")
self.assertEqual(CFGNodesStub.last_call["strength"], 0.8)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 6.5)
self.assertEqual(ModelAdvancedStub.last_call["model"], "MODEL::external-edit")
def test_qwen_edit_runtime_supports_2511_turbo_when_matching_lora_exists(self):
load_plugin_package()
from lls_node_test_qwen.qwen import discovery as qwen_discovery
from lls_node_test_qwen.qwen import runtime as qwen_runtime
stub = QwenFolderPathsStub()
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
qwen_runtime,
"folder_paths",
stub,
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
qwen_runtime,
"nodes_model_advanced",
ModelAdvancedStub,
), mock.patch.object(qwen_runtime, "nodes_qwen", QwenExtraNodesStub), mock.patch.object(
qwen_runtime,
"nodes_flux",
FluxNodesStub,
), mock.patch.object(qwen_runtime, "nodes_cfg", CFGNodesStub):
qwen_runtime.run_qwen_image_edit(
model_name="qwen_image_edit_2511_bf16.safetensors",
image="IMAGE::input",
image2=None,
image3=None,
prompt="turn the cat blue",
negative_prompt="",
steps=20,
seed=123,
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
cfg_norm_strength=1.0,
reference_latents_method="index_timestep_zero",
enable_turbo_mode=True,
turbo_lora_name="(auto)",
turbo_strength=0.7,
model="MODEL::external-edit",
)
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
self.assertEqual(
CoreQwenNodesStub.last_lora_calls[-1]["lora_name"],
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
)
self.assertEqual(CoreQwenNodesStub.last_lora_calls[-1]["strength_model"], 0.7)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 4)
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 1.0)
def test_qwen_text_node_executes_runtime(self):
load_plugin_package()
from lls_node_test_qwen.qwen import nodes as qwen_nodes
node = qwen_nodes.LLSQwenTextToImage()
with mock.patch.object(qwen_nodes.runtime, "run_qwen_text_to_image", return_value="IMAGE::node") as runtime_mock:
result = node.generate(
model_name="qwen_image_fp8_e4m3fn.safetensors",
prompt="a cat",
width=1024,
height=1024,
steps=20,
seed=1,
batch_size=1,
negative_prompt="",
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model="MODEL::external",
)
self.assertEqual(result, ("IMAGE::node",))
self.assertEqual(runtime_mock.call_args.kwargs["model"], "MODEL::external")
def test_qwen_edit_node_executes_runtime(self):
load_plugin_package()
from lls_node_test_qwen.qwen import nodes as qwen_nodes
node = qwen_nodes.LLSQwenImageEdit()
with mock.patch.object(qwen_nodes.runtime, "run_qwen_image_edit", return_value="IMAGE::node-edit") as runtime_mock:
result = node.generate(
model_name="qwen_image_edit_2511_bf16.safetensors",
image="IMAGE::input",
prompt="edit the cat",
steps=20,
seed=1,
image2=None,
image3=None,
negative_prompt="",
cfg=4.0,
sampler_name="euler",
scheduler="simple",
shift=3.1,
cfg_norm_strength=1.0,
reference_latents_method="index_timestep_zero",
enable_turbo_mode=False,
turbo_lora_name="(auto)",
turbo_strength=1.0,
model="MODEL::external",
)
self.assertEqual(result, ("IMAGE::node-edit",))
self.assertEqual(runtime_mock.call_args.kwargs["model"], "MODEL::external")
if __name__ == "__main__":
unittest.main()