diff --git a/prompt_manager.py b/prompt_manager.py index 5259d718..171debef 100644 --- a/prompt_manager.py +++ b/prompt_manager.py @@ -102,6 +102,7 @@ class PromptManager(PromptManagerBase, ComfyNodeABC): "The final combined text string (with prepend/append applied) that was encoded.", ) FUNCTION = "encode_prompt" + OUTPUT_NODE = True CATEGORY = "🫶 ComfyAssets/🧠 Prompts" DESCRIPTION = ( "Encodes a text prompt using a CLIP model into an embedding that can be used to guide " diff --git a/prompt_manager_text.py b/prompt_manager_text.py index 7c41051e..af5c4019 100644 --- a/prompt_manager_text.py +++ b/prompt_manager_text.py @@ -101,6 +101,7 @@ class PromptManagerText(PromptManagerBase, ComfyNodeABC): "The final combined text string (with prepend/append applied) ready for use in other nodes.", ) FUNCTION = "process_text" + OUTPUT_NODE = True CATEGORY = "🫶 ComfyAssets/🧠 Prompts" DESCRIPTION = ( "Processes and manages text prompts with database storage and search capabilities. " diff --git a/prompt_search_list.py b/prompt_search_list.py index 8a72310e..6910d563 100644 --- a/prompt_search_list.py +++ b/prompt_search_list.py @@ -108,6 +108,7 @@ class PromptSearchList(ComfyNodeABC): OUTPUT_IS_LIST = (True,) OUTPUT_TOOLTIPS = ("List of prompt texts matching the search criteria.",) FUNCTION = "search" + OUTPUT_NODE = True CATEGORY = "🫶 ComfyAssets/🧠 Prompts" DESCRIPTION = ( "Searches the prompt database and outputs matching prompts as a list. " diff --git a/pyproject.toml b/pyproject.toml index 662018a6..b80c54c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "promptmanager" description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite." -version = "3.1.1" +version = "3.1.2" license = {file = "LICENSE"} dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"] diff --git a/tests/test_node_execution_contract.py b/tests/test_node_execution_contract.py new file mode 100644 index 00000000..e7b5abbe --- /dev/null +++ b/tests/test_node_execution_contract.py @@ -0,0 +1,123 @@ +""" +Tests for ComfyUI node execution contract. + +Ensures all nodes are properly configured for ComfyUI's execution engine: +- OUTPUT_NODE = True so nodes are always included in the execution graph +- IS_CHANGED returns correct values for cache invalidation + +Regression tests for: https://github.com/ComfyAssets/ComfyUI_PromptManager/issues/120 +""" + +import os +import sys +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from prompt_manager import PromptManager +from prompt_manager_text import PromptManagerText +from prompt_search_list import PromptSearchList + +ALL_NODE_CLASSES = [PromptManager, PromptManagerText, PromptSearchList] + + +class TestOutputNode(unittest.TestCase): + """OUTPUT_NODE = True is required so ComfyUI always includes these nodes + in the execution graph. Without it, nodes with side effects (database + writes, execution tracking) can be silently skipped when downstream + nodes are cached.""" + + def test_prompt_manager_is_output_node(self): + self.assertIs(PromptManager.OUTPUT_NODE, True) + + def test_prompt_manager_text_is_output_node(self): + self.assertIs(PromptManagerText.OUTPUT_NODE, True) + + def test_prompt_search_list_is_output_node(self): + self.assertIs(PromptSearchList.OUTPUT_NODE, True) + + +class TestIsChangedPromptManager(unittest.TestCase): + """IS_CHANGED for PromptManager should return a deterministic hash + based on text inputs, so the node re-runs only when inputs change.""" + + def test_same_inputs_return_same_hash(self): + result1 = PromptManager.IS_CHANGED( + clip=None, text="hello", prepend_text="pre", append_text="post" + ) + result2 = PromptManager.IS_CHANGED( + clip=None, text="hello", prepend_text="pre", append_text="post" + ) + self.assertEqual(result1, result2) + + def test_different_text_returns_different_hash(self): + result1 = PromptManager.IS_CHANGED(clip=None, text="hello") + result2 = PromptManager.IS_CHANGED(clip=None, text="world") + self.assertNotEqual(result1, result2) + + def test_different_prepend_returns_different_hash(self): + result1 = PromptManager.IS_CHANGED(clip=None, text="hello", prepend_text="a") + result2 = PromptManager.IS_CHANGED(clip=None, text="hello", prepend_text="b") + self.assertNotEqual(result1, result2) + + def test_different_append_returns_different_hash(self): + result1 = PromptManager.IS_CHANGED(clip=None, text="hello", append_text="a") + result2 = PromptManager.IS_CHANGED(clip=None, text="hello", append_text="b") + self.assertNotEqual(result1, result2) + + def test_returns_string(self): + result = PromptManager.IS_CHANGED(clip=None, text="hello") + self.assertIsInstance(result, str) + + +class TestIsChangedPromptManagerText(unittest.TestCase): + """IS_CHANGED for PromptManagerText should behave identically to + PromptManager — deterministic hash based on text inputs.""" + + def test_same_inputs_return_same_hash(self): + result1 = PromptManagerText.IS_CHANGED( + text="hello", prepend_text="pre", append_text="post" + ) + result2 = PromptManagerText.IS_CHANGED( + text="hello", prepend_text="pre", append_text="post" + ) + self.assertEqual(result1, result2) + + def test_different_text_returns_different_hash(self): + result1 = PromptManagerText.IS_CHANGED(text="hello") + result2 = PromptManagerText.IS_CHANGED(text="world") + self.assertNotEqual(result1, result2) + + def test_different_prepend_returns_different_hash(self): + result1 = PromptManagerText.IS_CHANGED(text="hello", prepend_text="a") + result2 = PromptManagerText.IS_CHANGED(text="hello", prepend_text="b") + self.assertNotEqual(result1, result2) + + def test_different_append_returns_different_hash(self): + result1 = PromptManagerText.IS_CHANGED(text="hello", append_text="a") + result2 = PromptManagerText.IS_CHANGED(text="hello", append_text="b") + self.assertNotEqual(result1, result2) + + def test_returns_string(self): + result = PromptManagerText.IS_CHANGED(text="hello") + self.assertIsInstance(result, str) + + +class TestIsChangedPromptSearchList(unittest.TestCase): + """IS_CHANGED for PromptSearchList should always return a unique value + so the node re-runs every time (database contents may have changed).""" + + def test_returns_different_value_on_successive_calls(self): + result1 = PromptSearchList.IS_CHANGED() + time.sleep(0.01) + result2 = PromptSearchList.IS_CHANGED() + self.assertNotEqual(result1, result2) + + def test_returns_numeric(self): + result = PromptSearchList.IS_CHANGED() + self.assertIsInstance(result, float) + + +if __name__ == "__main__": + unittest.main()