177 lines
6.5 KiB
Python
177 lines
6.5 KiB
Python
"""Lightweight regression tests for the local ThinkingLLM fork.
|
|
|
|
Run from the ComfyUI-ThinkingLLM folder:
|
|
python tests/test_new_features.py
|
|
|
|
These tests intentionally avoid importing the model modules so they can run in a
|
|
plain shell without ML dependencies.
|
|
"""
|
|
|
|
import ast
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
PKG = HERE.parent
|
|
|
|
STREAMING_FILES = {
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py": {
|
|
"stage": "INITIAL GENERATION",
|
|
"label": "QwenVL GGUF",
|
|
},
|
|
"AILab_QwenVL_PromptEnhancer.py": {
|
|
"stage": "HF TEXT GENERATION",
|
|
"label": "QwenVL HF",
|
|
},
|
|
"AILab_QwenVL.py": {
|
|
"stage": None,
|
|
"label": "QwenVL HF",
|
|
},
|
|
"AILab_QwenVL_GGUF.py": {
|
|
"stage": "STREAMING",
|
|
"label": "QwenVL GGUF",
|
|
},
|
|
}
|
|
|
|
|
|
def read_source(filename: str) -> str:
|
|
return (PKG / filename).read_text(encoding="utf-8")
|
|
|
|
|
|
def parse_source(filename: str) -> ast.AST:
|
|
return ast.parse(read_source(filename), filename=filename)
|
|
|
|
|
|
class TestTerminalDisplayHelper(unittest.TestCase):
|
|
def test_helper_file_exists(self):
|
|
self.assertTrue((PKG / "AILab_StreamDisplay.py").exists())
|
|
|
|
def test_helper_class_and_methods_exist(self):
|
|
tree = parse_source("AILab_StreamDisplay.py")
|
|
helper_class = None
|
|
for node in tree.body:
|
|
if isinstance(node, ast.ClassDef) and node.name == "TerminalStreamDisplay":
|
|
helper_class = node
|
|
break
|
|
self.assertIsNotNone(helper_class, "TerminalStreamDisplay class missing")
|
|
method_names = {
|
|
node.name for node in helper_class.body if isinstance(node, ast.FunctionDef)
|
|
}
|
|
for method in ["start_stage", "push", "flush", "end_stage"]:
|
|
self.assertIn(method, method_names)
|
|
|
|
|
|
class TestBufferedStreamingIntegration(unittest.TestCase):
|
|
def test_old_rolling_tail_patterns_removed(self):
|
|
old_patterns = [
|
|
r'print\(f"\\r\{tail\}"',
|
|
r"tail\s*=\s*full_streamed\[-200:\]",
|
|
r"tail\s*=\s*full_text\[-200:\]",
|
|
]
|
|
for filename in STREAMING_FILES:
|
|
source = read_source(filename)
|
|
for pattern in old_patterns:
|
|
with self.subTest(filename=filename, pattern=pattern):
|
|
self.assertIsNone(re.search(pattern, source))
|
|
|
|
def test_all_streaming_files_use_helper(self):
|
|
for filename, expectations in STREAMING_FILES.items():
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("TerminalStreamDisplay", source)
|
|
self.assertIn(expectations["label"], source)
|
|
if expectations["stage"]:
|
|
self.assertIn(expectations["stage"], source)
|
|
# Accept either the old buffered API or compact rolling-tail API
|
|
self.assertTrue(
|
|
".start_stage(" in source or ".push_compact(" in source,
|
|
f"No streaming API usage found in {filename}: "
|
|
"expected .start_stage() or .push_compact()"
|
|
)
|
|
|
|
|
|
class TestPromptEnhancerMetadata(unittest.TestCase):
|
|
def test_hf_prompt_enhancer_metadata_matches_runtime(self):
|
|
source = read_source("AILab_QwenVL_PromptEnhancer.py")
|
|
self.assertIn('RETURN_TYPES = ("STRING", "STRING")', source)
|
|
self.assertIn('RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")', source)
|
|
self.assertIn('"stream_tokens_to_terminal": ("BOOLEAN"', source)
|
|
self.assertIn('"unique_id": "UNIQUE_ID"', source)
|
|
self.assertIn('"extra_pnginfo": "EXTRA_PNGINFO"', source)
|
|
|
|
def test_raw_trace_still_exposed_across_nodes(self):
|
|
expectations = {
|
|
"AILab_QwenVL_PromptEnhancer.py": 'RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")',
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py": 'RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")',
|
|
"AILab_QwenVL.py": 'RETURN_NAMES = ("RESPONSE", "RAW_TRACE")',
|
|
"AILab_QwenVL_GGUF.py": 'RETURN_NAMES = ("RESPONSE", "RAW_TRACE")',
|
|
}
|
|
for filename, marker in expectations.items():
|
|
with self.subTest(filename=filename):
|
|
self.assertIn(marker, read_source(filename))
|
|
|
|
|
|
class TestInterruptSupport(unittest.TestCase):
|
|
INTERRUPT_FILES = [
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
|
|
def test_interrupt_import_present(self):
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("from comfy.model_management import throw_exception_if_processing_interrupted", source)
|
|
|
|
def test_interrupt_check_in_loops(self):
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("throw_exception_if_processing_interrupted()", source)
|
|
|
|
def test_interrupt_after_blocking_calls(self):
|
|
"""Verify interrupt checks exist after non-streaming generate/create_chat_completion calls."""
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
# Every file should have at least one interrupt check outside streaming loops
|
|
# (after blocking calls + in stream loops = at least 2 checks per file,
|
|
# but simple nodes may have only 1 non-streaming path)
|
|
count = source.count("throw_exception_if_processing_interrupted()")
|
|
self.assertGreaterEqual(
|
|
count, 1,
|
|
f"{filename} has only {count} interrupt check(s); expected >= 1"
|
|
)
|
|
|
|
|
|
class TestNoResidualGlobals(unittest.TestCase):
|
|
def test_no_global_last_saved_prompt_statement(self):
|
|
for filename in [
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]:
|
|
tree = parse_source(filename)
|
|
visitor = _GlobalCheck()
|
|
visitor.visit(tree)
|
|
with self.subTest(filename=filename):
|
|
self.assertFalse(visitor.found)
|
|
|
|
|
|
class _GlobalCheck(ast.NodeVisitor):
|
|
def __init__(self):
|
|
self.found = False
|
|
|
|
def visit_Global(self, node):
|
|
if "LAST_SAVED_PROMPT" in node.names:
|
|
self.found = True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|