diff --git a/src/backend/tests/unit/base/tools/test_run_flow.py b/src/backend/tests/unit/base/tools/test_run_flow.py index 6f7f8d63c6..d967f37e73 100644 --- a/src/backend/tests/unit/base/tools/test_run_flow.py +++ b/src/backend/tests/unit/base/tools/test_run_flow.py @@ -631,6 +631,79 @@ class TestRunFlowBaseComponentInputOutputHandling: assert updated[1]["input_types"] == ["str"] assert updated[2]["input_types"] == [] # Should be added as empty list + def test_resolve_exposed_input_types_restores_message_for_empty_text_field(self): + """Empty input_types on a text field should be restored to ["Message"].""" + entry = {"type": "str", "input_types": []} + + assert RunFlowBaseComponent._resolve_exposed_input_types(entry) == ["Message"] + + def test_resolve_exposed_input_types_restores_message_when_missing(self): + """Missing input_types on a text field should default to ["Message"].""" + entry = {"type": "Text"} + + assert RunFlowBaseComponent._resolve_exposed_input_types(entry) == ["Message"] + + def test_resolve_exposed_input_types_preserves_existing_input_types(self): + """Existing input_types must be preserved without modification.""" + entry = {"type": "str", "input_types": ["Message", "Data"]} + + result = RunFlowBaseComponent._resolve_exposed_input_types(entry) + + assert result == ["Message", "Data"] + # Returns a copy, not the original list, to avoid accidental shared mutation. + assert result is not entry["input_types"] + + def test_resolve_exposed_input_types_skips_non_text_fields(self): + """Non-text fields with empty input_types are left untouched.""" + entry = {"type": "bool", "input_types": []} + + assert RunFlowBaseComponent._resolve_exposed_input_types(entry) == [] + + def test_get_new_fields_exposes_message_handle_for_chat_input_value(self): + """Run Flow must expose a Message handle for ChatInput-style input_value fields. + + Regression test for LE-1233: ChatInput sets input_types=[] on its input_value, + which previously propagated to the Run Flow dynamic field and prevented users + from wiring upstream components into the exposed Input Text field. + """ + component = RunFlowBaseComponent() + + chat_input_vertex = MagicMock(spec=Vertex) + chat_input_vertex.id = "ChatInput-abcde" + chat_input_vertex.display_name = "Chat Input" + chat_input_vertex.data = { + "node": { + "template": { + "input_value": { + "name": "input_value", + "display_name": "Input Text", + "type": "str", + "input_types": [], + "advanced": False, + }, + "should_store_message": { + "name": "should_store_message", + "display_name": "Store Messages", + "type": "bool", + "input_types": [], + "advanced": True, + }, + }, + "field_order": ["input_value", "should_store_message"], + } + } + + new_fields = component.get_new_fields([chat_input_vertex]) + + input_value_field = next(f for f in new_fields if f["name"].endswith("~input_value")) + bool_field = next(f for f in new_fields if f["name"].endswith("~should_store_message")) + + assert input_value_field["input_types"] == ["Message"], ( + "Text-typed exposed inputs must receive a Message handle so upstream components can be wired in (LE-1233)." + ) + # Bool fields don't expose a Message handle — handle visibility is driven by `type`. + assert bool_field["input_types"] == [] + class TestRunFlowBaseComponentOutputMethods: """Test output methods.""" diff --git a/src/lfx/src/lfx/base/tools/run_flow.py b/src/lfx/src/lfx/base/tools/run_flow.py index 730d3de4b8..a2208254ef 100644 --- a/src/lfx/src/lfx/base/tools/run_flow.py +++ b/src/lfx/src/lfx/base/tools/run_flow.py @@ -203,6 +203,7 @@ class RunFlowBaseComponent(Component): ), # TODO: make this more robust? "tool_mode": not (field_template[input_name].get("advanced", False)), + "input_types": self._resolve_exposed_input_types(field_template[input_name]), } ) for input_name in field_order @@ -211,6 +212,23 @@ class RunFlowBaseComponent(Component): new_fields.extend(new_vertex_inputs) return new_fields + @staticmethod + def _resolve_exposed_input_types(field_template_entry: dict) -> list[str]: + """Resolve input_types for a flow input re-exposed via Run Flow. + + Some components (e.g. ChatInput) explicitly set ``input_types=[]`` on + text-typed inputs to suppress connection handles within their own + flow. When Run Flow re-exposes those fields the user expects to wire + upstream components into them, so we restore a Message handle for + text-typed fields with no input_types. + """ + existing = field_template_entry.get("input_types") or [] + if existing: + return list(existing) + if field_template_entry.get("type") in {"str", "Text"}: + return ["Message"] + return list(existing) + def add_new_fields(self, build_config: dotdict, new_fields: list[dotdict]) -> dotdict: """Add new fields to the build_config.""" for field in new_fields: