From 608fc5df4df42d8e3bdcc1ddcd4cfb57e278cb57 Mon Sep 17 00:00:00 2001 From: seekmistar01 Date: Sun, 28 Jun 2026 12:15:29 +0800 Subject: [PATCH] fix(agent): Switch no longer matches an empty condition (all([]) is True) (#15644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the agent `Switch` component matching an **empty/all-skipped condition** unconditionally because `all([]) is True`. ## Root cause `res` only accumulates for items with a non-empty `cpn_id` (blank ones `continue`). For a condition with empty `items` (or all-blank `cpn_id`), `res == []`, and `if all(res):` is `True`, so the Switch routes to that condition's `to` target before reaching the else/`end_cpn_ids` branch. ## Fix ```diff - if all(res): + if res and all(res): ``` An empty result set no longer counts as a match; genuinely-satisfied "and" conditions still route (the real `all(res)` path is preserved). ## Files changed - `agent/component/switch.py` - `test/unit_test/agent/component/test_switch_empty_condition.py` (new) ## Verification - `ruff check` / `ruff format --check` — clean - Added unit tests (mirroring the existing `_FakeCanvas` component-test pattern): an empty/all-skipped "and" condition now falls through to `end_cpn_ids`; a genuinely-satisfied "and" condition still routes to its target. - Local full pytest not run (heavy RAG deps); CI validates. ## Note Implemented with LLM assistance (model: claude-opus-4-8). Closes #15643 --------- Co-authored-by: seekmistar01 Co-authored-by: Claude Opus 4.8 Co-authored-by: Zhichang Yu --- .../component/test_switch_empty_condition.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/unit_test/agent/component/test_switch_empty_condition.py diff --git a/test/unit_test/agent/component/test_switch_empty_condition.py b/test/unit_test/agent/component/test_switch_empty_condition.py new file mode 100644 index 0000000000..b4d6de5a8e --- /dev/null +++ b/test/unit_test/agent/component/test_switch_empty_condition.py @@ -0,0 +1,94 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unit tests for ``Switch`` condition evaluation. + +Regression: an "and" condition whose ``items`` are all skipped (empty +``cpn_id``) leaves the per-condition result list empty, and ``all([]) is True``, +so the Switch matched that condition unconditionally and routed to it before +ever reaching the else/end branch. +""" + +from types import SimpleNamespace + +import pytest + +from agent.component.switch import Switch + + +pytestmark = [pytest.mark.p1] + + +class _FakeCanvas: + def __init__(self, refs=None): + self._refs = refs or {} + + def get_variable_value(self, token): + return self._refs.get(token) + + def get_component_name(self, cpn_id): + return cpn_id + + +def _make_switch(conditions, end_cpn_ids, refs=None): + sw = Switch.__new__(Switch) + sw._param = SimpleNamespace(conditions=conditions, end_cpn_ids=end_cpn_ids) + sw._canvas = _FakeCanvas(refs) + sw.check_if_canceled = lambda _msg: False + sw.set_input_value = lambda *_args, **_kwargs: None + outputs = {} + sw.set_output = lambda key, value: outputs.__setitem__(key, value) + sw._invoke() + return outputs + + +def test_empty_condition_falls_through_to_end(): + # The only item is skipped (blank cpn_id) -> result list is empty. + conditions = [ + { + "logical_operator": "and", + "items": [{"cpn_id": "", "operator": "contains", "value": "x"}], + "to": ["TARGET"], + } + ] + outputs = _make_switch(conditions, end_cpn_ids=["END"]) + assert outputs["_next"] == ["END"] + + +def test_empty_and_items_fall_through_to_end(): + conditions = [ + { + "logical_operator": "and", + "items": [], + "to": ["TARGET"], + } + ] + outputs = _make_switch(conditions, end_cpn_ids=["END"]) + assert outputs["_next"] == ["END"] + + +def test_satisfied_and_condition_still_routes(): + # A genuinely satisfied "and" condition must still route to its target + # (the fix must not break the real all(res) path). + conditions = [ + { + "logical_operator": "and", + "items": [{"cpn_id": "c@out", "operator": "contains", "value": "hello"}], + "to": ["TARGET"], + } + ] + outputs = _make_switch(conditions, end_cpn_ids=["END"], refs={"c@out": "hello world"}) + assert outputs["_next"] == ["TARGET"]