From e9bfff44ceda0fb15f9cd3072d895c9d7d59db4e Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Fri, 10 Apr 2026 15:11:54 +0800 Subject: [PATCH] Preserve legacy string-node search during Text rename The string-node rename updated display names for discoverability, but `StringLength` lost the exact legacy `Length` alias. This restores that alias and adds focused tests for the renamed schema metadata so future renames keep old search paths intact. Constraint: Keep scope limited to the recent string-node rename surface Rejected: Broad schema metadata audit across unrelated nodes | exceeds the changed-area scope for this automation Confidence: high Scope-risk: narrow Reversibility: clean Directive: When renaming nodes for search/discoverability, preserve the previous display name as an alias and lock it with a schema test Tested: uvx pytest tests-unit/comfy_extras_test/nodes_string_test.py Not-tested: Ruff lint, because uvx temporary download stalled in this environment --- comfy_extras/nodes_string.py | 2 +- .../comfy_extras_test/nodes_string_test.py | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tests-unit/comfy_extras_test/nodes_string_test.py diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py index 75a8bb4ee..fc0806330 100644 --- a/comfy_extras/nodes_string.py +++ b/comfy_extras/nodes_string.py @@ -55,7 +55,7 @@ class StringLength(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="StringLength", - search_aliases=["character count", "text size", "string length"], + search_aliases=["Length", "character count", "text size", "string length"], display_name="Text Length", category="utils/string", inputs=[ diff --git a/tests-unit/comfy_extras_test/nodes_string_test.py b/tests-unit/comfy_extras_test/nodes_string_test.py new file mode 100644 index 000000000..129dd2c50 --- /dev/null +++ b/tests-unit/comfy_extras_test/nodes_string_test.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass, field +from types import ModuleType, SimpleNamespace +from unittest.mock import patch +import importlib +import sys + + +@dataclass +class _Schema: + node_id: str + display_name: str | None = None + category: str = "sd" + inputs: list = field(default_factory=list) + outputs: list = field(default_factory=list) + hidden: list = field(default_factory=list) + description: str = "" + search_aliases: list[str] = field(default_factory=list) + + +class _FieldFactory: + @staticmethod + def Input(*args, **kwargs): + return {"args": args, "kwargs": kwargs} + + @staticmethod + def Output(*args, **kwargs): + return {"args": args, "kwargs": kwargs} + + +def _import_nodes_string(): + fake_io = SimpleNamespace( + Schema=_Schema, + String=_FieldFactory, + Int=_FieldFactory, + Boolean=_FieldFactory, + Combo=_FieldFactory, + NodeOutput=lambda value: (value,), + ComfyNode=object, + ) + fake_latest = ModuleType("comfy_api.latest") + fake_latest.ComfyExtension = object + fake_latest.io = fake_io + + fake_comfy_api = ModuleType("comfy_api") + fake_comfy_api.latest = fake_latest + fake_typing_extensions = ModuleType("typing_extensions") + fake_typing_extensions.override = lambda func: func + + with patch.dict( + sys.modules, + { + "comfy_api": fake_comfy_api, + "comfy_api.latest": fake_latest, + "typing_extensions": fake_typing_extensions, + }, + ): + sys.modules.pop("comfy_extras.nodes_string", None) + return importlib.import_module("comfy_extras.nodes_string") + + +nodes_string = _import_nodes_string() + + +class TestStringNodeSchemaRenames: + def test_text_prefix_display_names_are_exposed(self): + cases = [ + (nodes_string.StringConcatenate, "Text Concatenate"), + (nodes_string.StringSubstring, "Text Substring"), + (nodes_string.StringLength, "Text Length"), + (nodes_string.CaseConverter, "Text Case Converter"), + (nodes_string.StringTrim, "Text Trim"), + (nodes_string.StringReplace, "Text Replace"), + (nodes_string.StringContains, "Text Contains"), + (nodes_string.StringCompare, "Text Compare"), + (nodes_string.RegexMatch, "Text Match"), + (nodes_string.RegexExtract, "Text Extract Substring"), + (nodes_string.RegexReplace, "Text Replace (Regex)"), + ] + + for node_cls, expected_name in cases: + assert node_cls.define_schema().display_name == expected_name + + def test_old_display_names_remain_searchable(self): + cases = [ + (nodes_string.StringConcatenate, "Concatenate"), + (nodes_string.StringSubstring, "Substring"), + (nodes_string.StringLength, "Length"), + (nodes_string.CaseConverter, "Case Converter"), + (nodes_string.StringTrim, "Trim"), + (nodes_string.StringReplace, "Replace"), + (nodes_string.StringContains, "Contains"), + (nodes_string.StringCompare, "Compare"), + (nodes_string.RegexMatch, "Regex Match"), + (nodes_string.RegexExtract, "Regex Extract"), + (nodes_string.RegexReplace, "Regex Replace"), + ] + + for node_cls, expected_alias in cases: + assert expected_alias in node_cls.define_schema().search_aliases + + def test_regex_nodes_keep_regex_search_keyword(self): + regex_nodes = [ + nodes_string.RegexMatch, + nodes_string.RegexExtract, + nodes_string.RegexReplace, + ] + + for node_cls in regex_nodes: + assert "regex" in node_cls.define_schema().search_aliases