From 34e4fad8632a70178d5cfa2e96ac5fa1af8902b1 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:30:32 +0800 Subject: [PATCH 1/6] fix: clear stale socket references on neighbors in remove_component `Pipeline.remove_component` reset only the removed component's own sockets, but left the removed component's name in its neighbors' sockets: a downstream component kept it in its input socket's `senders`, and an upstream component kept it in its output socket's `receivers`. `graph.remove_node` drops the edges but does not touch the socket objects held by the surviving components. These dangling references corrupt pipeline introspection and validation: `Pipeline.inputs()` computes `is_mandatory = socket.is_mandatory and not socket.senders`, so a stale sender hides a now-unconnected mandatory input; and `validate_input` raises a spurious "already connected to component ..." error (naming the deleted component) when that input is fed directly. Strip the removed component's name from its neighbors' sockets, walking the incident edges before `remove_node` while they still carry the socket objects. Co-Authored-By: Claude Opus 4.8 --- haystack/core/pipeline/base.py | 10 ++++++ ...ponent-stale-sockets-592cf97cb03ad526.yaml | 11 ++++++ test/core/pipeline/test_pipeline_base.py | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml diff --git a/haystack/core/pipeline/base.py b/haystack/core/pipeline/base.py index f5cb777ec1e..1077dfe09e8 100644 --- a/haystack/core/pipeline/base.py +++ b/haystack/core/pipeline/base.py @@ -468,6 +468,16 @@ def remove_component(self, name: str) -> Component: ", ".join(n for n in self.graph.nodes), ) from exc + # Strip the removed component's name from the sockets of its neighbors, so no + # dangling references remain on the surviving components (this must happen before + # remove_node, while the incident edges still carry the socket objects). + for _, _, edge_data in self.graph.in_edges(name, data=True): + sender_socket = edge_data["from_socket"] + sender_socket.receivers = [r for r in sender_socket.receivers if r != name] + for _, _, edge_data in self.graph.out_edges(name, data=True): + receiver_socket = edge_data["to_socket"] + receiver_socket.senders = [s for s in receiver_socket.senders if s != name] + # Delete component from the graph, deleting all its connections self.graph.remove_node(name) diff --git a/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml b/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml new file mode 100644 index 00000000000..a1b559fbfe6 --- /dev/null +++ b/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Fixed `Pipeline.remove_component` leaving dangling references to the removed + component on the sockets of its neighboring components. Previously, removing a + component reset only its own sockets, so a surviving neighbor kept the removed + component's name in its input socket's `senders` (or output socket's + `receivers`). This corrupted introspection and validation: `Pipeline.inputs()` + hid a now-unconnected mandatory input, and feeding that input directly could + raise a spurious "already connected" error. The removed component's name is now + stripped from its neighbors' sockets as well. diff --git a/test/core/pipeline/test_pipeline_base.py b/test/core/pipeline/test_pipeline_base.py index bba743160a3..805bf293191 100644 --- a/test/core/pipeline/test_pipeline_base.py +++ b/test/core/pipeline/test_pipeline_base.py @@ -269,6 +269,40 @@ def test_remove_component_removes_component_and_its_edges(self): assert sorted(pipe.graph.nodes) == ["1", "3", "4"] assert sorted([(u, v) for (u, v) in pipe.graph.edges()]) == [("3", "4")] + def test_remove_component_clears_stale_senders_on_downstream_neighbor(self): + pipe = PipelineBase() + Producer = component_class("Producer", output_types={"value": int}) + Consumer = component_class("Consumer", input_types={"value": int}) + pipe.add_component("producer", Producer()) + pipe.add_component("consumer", Consumer()) + pipe.connect("producer.value", "consumer.value") + + pipe.remove_component("producer") + + consumer = pipe.get_component("consumer") + consumer_socket = consumer.__haystack_input__._sockets_dict["value"] + # The surviving neighbor must not keep a dangling reference to the removed component. + assert consumer_socket.senders == [] + # With the stale sender gone, the now-unconnected mandatory input is exposed again. + assert pipe.inputs() == {"consumer": {"value": {"type": int, "is_mandatory": True}}} + # Feeding it directly must not raise a spurious "already connected" error. + pipe.validate_input({"consumer": {"value": 5}}) + + def test_remove_component_clears_stale_receivers_on_upstream_neighbor(self): + pipe = PipelineBase() + Producer = component_class("Producer", output_types={"value": int}) + Consumer = component_class("Consumer", input_types={"value": int}) + pipe.add_component("producer", Producer()) + pipe.add_component("consumer", Consumer()) + pipe.connect("producer.value", "consumer.value") + + pipe.remove_component("consumer") + + producer = pipe.get_component("producer") + producer_socket = producer.__haystack_output__._sockets_dict["value"] + # The surviving upstream neighbor must not keep a dangling receiver reference. + assert producer_socket.receivers == [] + def test_remove_component_allows_you_to_reuse_the_component(self): pipe = PipelineBase() Some = component_class("Some", input_types={"in": int}, output_types={"out": int}) From f82d8aee46506e1085db62d50e3ae6b4dc3bbf74 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:35:30 +0800 Subject: [PATCH 2/6] docs: use double backticks in release note for reno RST check Co-Authored-By: Claude Opus 4.8 --- ...fix-remove-component-stale-sockets-592cf97cb03ad526.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml b/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml index a1b559fbfe6..368145af626 100644 --- a/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml +++ b/releasenotes/notes/fix-remove-component-stale-sockets-592cf97cb03ad526.yaml @@ -1,11 +1,11 @@ --- fixes: - | - Fixed `Pipeline.remove_component` leaving dangling references to the removed + Fixed ``Pipeline.remove_component`` leaving dangling references to the removed component on the sockets of its neighboring components. Previously, removing a component reset only its own sockets, so a surviving neighbor kept the removed - component's name in its input socket's `senders` (or output socket's - `receivers`). This corrupted introspection and validation: `Pipeline.inputs()` + component's name in its input socket's ``senders`` (or output socket's + ``receivers``). This corrupted introspection and validation: ``Pipeline.inputs()`` hid a now-unconnected mandatory input, and feeding that input directly could raise a spurious "already connected" error. The removed component's name is now stripped from its neighbors' sockets as well. From 110264ed69a1bd3a2d409b24b2fc1f26ac0f3c28 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:50:58 +0800 Subject: [PATCH 3/6] fix: silence mypy attr-defined on __haystack_input__/__haystack_output__ in new tests Matches the existing # type: ignore[attr-defined] convention already used elsewhere in this file for the same dunder-attribute access pattern. Co-Authored-By: Claude Opus 4.8 --- test/core/pipeline/test_pipeline_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/pipeline/test_pipeline_base.py b/test/core/pipeline/test_pipeline_base.py index 805bf293191..bfdffe7d295 100644 --- a/test/core/pipeline/test_pipeline_base.py +++ b/test/core/pipeline/test_pipeline_base.py @@ -280,7 +280,7 @@ def test_remove_component_clears_stale_senders_on_downstream_neighbor(self): pipe.remove_component("producer") consumer = pipe.get_component("consumer") - consumer_socket = consumer.__haystack_input__._sockets_dict["value"] + consumer_socket = consumer.__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] # The surviving neighbor must not keep a dangling reference to the removed component. assert consumer_socket.senders == [] # With the stale sender gone, the now-unconnected mandatory input is exposed again. @@ -299,7 +299,7 @@ def test_remove_component_clears_stale_receivers_on_upstream_neighbor(self): pipe.remove_component("consumer") producer = pipe.get_component("producer") - producer_socket = producer.__haystack_output__._sockets_dict["value"] + producer_socket = producer.__haystack_output__._sockets_dict["value"] # type: ignore[attr-defined] # The surviving upstream neighbor must not keep a dangling receiver reference. assert producer_socket.receivers == [] From c9cd146643d562891af6a99f1aa6bd0da35c9878 Mon Sep 17 00:00:00 2001 From: "David S. Batista" Date: Wed, 22 Jul 2026 13:24:02 +0200 Subject: [PATCH 4/6] adding more tests to cover edge cases --- haystack/core/pipeline/base.py | 5 +- test/core/pipeline/test_pipeline_base.py | 79 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/haystack/core/pipeline/base.py b/haystack/core/pipeline/base.py index 1077dfe09e8..0784771745c 100644 --- a/haystack/core/pipeline/base.py +++ b/haystack/core/pipeline/base.py @@ -468,9 +468,8 @@ def remove_component(self, name: str) -> Component: ", ".join(n for n in self.graph.nodes), ) from exc - # Strip the removed component's name from the sockets of its neighbors, so no - # dangling references remain on the surviving components (this must happen before - # remove_node, while the incident edges still carry the socket objects). + # Remove this component's name from its neighbors' sockets before the edges are gone, + # otherwise the surviving components are left holding dangling references to it. for _, _, edge_data in self.graph.in_edges(name, data=True): sender_socket = edge_data["from_socket"] sender_socket.receivers = [r for r in sender_socket.receivers if r != name] diff --git a/test/core/pipeline/test_pipeline_base.py b/test/core/pipeline/test_pipeline_base.py index bfdffe7d295..cae6dd5b394 100644 --- a/test/core/pipeline/test_pipeline_base.py +++ b/test/core/pipeline/test_pipeline_base.py @@ -303,6 +303,85 @@ def test_remove_component_clears_stale_receivers_on_upstream_neighbor(self): # The surviving upstream neighbor must not keep a dangling receiver reference. assert producer_socket.receivers == [] + def test_remove_component_middle_of_chain_removal(self): + """Removing a component that is both a downstream and an upstream neighbor cleans up both sides.""" + pipe = PipelineBase() + producer_class = component_class("Producer", output_types={"value": int}) + middle_class = component_class("Middle", input_types={"value": int}, output_types={"value": int}) + consumer_class = component_class("Consumer", input_types={"value": int}) + pipe.add_component("producer", producer_class()) + pipe.add_component("middle", middle_class()) + pipe.add_component("consumer", consumer_class()) + pipe.connect("producer.value", "middle.value") + pipe.connect("middle.value", "consumer.value") + + pipe.remove_component("middle") + + producer_socket = pipe.get_component("producer").__haystack_output__._sockets_dict["value"] # type: ignore[attr-defined] + consumer_socket = pipe.get_component("consumer").__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] + # Both the upstream and the downstream neighbor must be cleaned of the removed component. + assert producer_socket.receivers == [] + assert consumer_socket.senders == [] + + def test_remove_component_fan_out(self): + """Removing a sender that fans out to multiple receivers cleans up every receiver, not just one.""" + pipe = PipelineBase() + producer_class = component_class("Producer", output_types={"value": int}) + consumer_a_class = component_class("ConsumerA", input_types={"value": int}) + consumer_b_class = component_class("ConsumerB", input_types={"value": int}) + pipe.add_component("producer", producer_class()) + pipe.add_component("consumer_a", consumer_a_class()) + pipe.add_component("consumer_b", consumer_b_class()) + pipe.connect("producer.value", "consumer_a.value") + pipe.connect("producer.value", "consumer_b.value") + + pipe.remove_component("producer") + + consumer_a_socket = pipe.get_component("consumer_a").__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] + consumer_b_socket = pipe.get_component("consumer_b").__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] + # Every receiver of the removed sender's output must be cleaned, not just the first one. + assert consumer_a_socket.senders == [] + assert consumer_b_socket.senders == [] + + def test_remove_component_variadic_multi_sender(self): + """Removing one sender of a variadic receiver only strips itself, leaving the other senders intact.""" + pipe = PipelineBase() + producer_a_class = component_class("ProducerA", output_types={"value": int}) + producer_b_class = component_class("ProducerB", output_types={"value": int}) + consumer_class = component_class("Consumer", input_types={"value": list[int]}) + pipe.add_component("producer_a", producer_a_class()) + pipe.add_component("producer_b", producer_b_class()) + pipe.add_component("consumer", consumer_class()) + pipe.connect("producer_a.value", "consumer.value") + pipe.connect("producer_b.value", "consumer.value") + + consumer_socket = pipe.get_component("consumer").__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] + assert consumer_socket.is_variadic + assert sorted(consumer_socket.senders) == ["producer_a", "producer_b"] + + pipe.remove_component("producer_a") + + # Only the removed sender is stripped; the surviving sender must remain in the list. + assert consumer_socket.senders == ["producer_b"] + + def test_remove_component_multiple_connections_to_same_neighbor(self): + """Removing a component with several parallel connections to the same neighbor cleans up all of them.""" + pipe = PipelineBase() + producer_class = component_class("Producer", output_types={"first": int, "second": int}) + consumer_class = component_class("Consumer", input_types={"first": int, "second": int}) + pipe.add_component("producer", producer_class()) + pipe.add_component("consumer", consumer_class()) + pipe.connect("producer.first", "consumer.first") + pipe.connect("producer.second", "consumer.second") + + pipe.remove_component("producer") + + consumer_first_socket = pipe.get_component("consumer").__haystack_input__._sockets_dict["first"] # type: ignore[attr-defined] + consumer_second_socket = pipe.get_component("consumer").__haystack_input__._sockets_dict["second"] # type: ignore[attr-defined] + # Both parallel connections between the same pair of components must be cleaned up. + assert consumer_first_socket.senders == [] + assert consumer_second_socket.senders == [] + def test_remove_component_allows_you_to_reuse_the_component(self): pipe = PipelineBase() Some = component_class("Some", input_types={"in": int}, output_types={"out": int}) From 6161f7c8e9400feb2fce8dc64159f40e21b91345 Mon Sep 17 00:00:00 2001 From: "David S. Batista" Date: Wed, 22 Jul 2026 13:42:08 +0200 Subject: [PATCH 5/6] fixing type issues --- test/core/pipeline/test_pipeline_base.py | 59 ++++++++++++++++++------ 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/test/core/pipeline/test_pipeline_base.py b/test/core/pipeline/test_pipeline_base.py index cae6dd5b394..52e10567514 100644 --- a/test/core/pipeline/test_pipeline_base.py +++ b/test/core/pipeline/test_pipeline_base.py @@ -271,10 +271,10 @@ def test_remove_component_removes_component_and_its_edges(self): def test_remove_component_clears_stale_senders_on_downstream_neighbor(self): pipe = PipelineBase() - Producer = component_class("Producer", output_types={"value": int}) - Consumer = component_class("Consumer", input_types={"value": int}) - pipe.add_component("producer", Producer()) - pipe.add_component("consumer", Consumer()) + producer_class = component_class("Producer", output_types={"value": int}) + consumer_class = component_class("Consumer", input_types={"value": int}) + pipe.add_component("producer", producer_class()) + pipe.add_component("consumer", consumer_class()) pipe.connect("producer.value", "consumer.value") pipe.remove_component("producer") @@ -290,10 +290,10 @@ def test_remove_component_clears_stale_senders_on_downstream_neighbor(self): def test_remove_component_clears_stale_receivers_on_upstream_neighbor(self): pipe = PipelineBase() - Producer = component_class("Producer", output_types={"value": int}) - Consumer = component_class("Consumer", input_types={"value": int}) - pipe.add_component("producer", Producer()) - pipe.add_component("consumer", Consumer()) + producer_class = component_class("Producer", output_types={"value": int}) + consumer_class = component_class("Consumer", input_types={"value": int}) + pipe.add_component("producer", producer_class()) + pipe.add_component("consumer", consumer_class()) pipe.connect("producer.value", "consumer.value") pipe.remove_component("consumer") @@ -343,6 +343,37 @@ def test_remove_component_fan_out(self): assert consumer_a_socket.senders == [] assert consumer_b_socket.senders == [] + def test_remove_component_fan_in(self): + """Removing a receiver fed by multiple senders cleans up every sender, not just one.""" + pipe = PipelineBase() + producer_a_class = component_class("ProducerA", output_types={"value": int}) + producer_b_class = component_class("ProducerB", output_types={"value": int}) + consumer_class = component_class("Consumer", input_types={"value": list[int]}) + pipe.add_component("producer_a", producer_a_class()) + pipe.add_component("producer_b", producer_b_class()) + pipe.add_component("consumer", consumer_class()) + pipe.connect("producer_a.value", "consumer.value") + pipe.connect("producer_b.value", "consumer.value") + + pipe.remove_component("consumer") + + producer_a_socket = pipe.get_component("producer_a").__haystack_output__._sockets_dict["value"] # type: ignore[attr-defined] + producer_b_socket = pipe.get_component("producer_b").__haystack_output__._sockets_dict["value"] # type: ignore[attr-defined] + # Every sender of the removed receiver must be cleaned, not just the first one. + assert producer_a_socket.receivers == [] + assert producer_b_socket.receivers == [] + + def test_remove_component_with_no_connections(self): + """Removing a component with no connections at all is a safe no-op over its (empty) edge sets.""" + pipe = PipelineBase() + some_class = component_class("Some", input_types={"in": int}, output_types={"out": int}) + pipe.add_component("isolated", some_class()) + + removed = pipe.remove_component("isolated") + + assert "isolated" not in pipe.graph.nodes + assert removed.__haystack_added_to_pipeline__ is None # type: ignore[attr-defined] + def test_remove_component_variadic_multi_sender(self): """Removing one sender of a variadic receiver only strips itself, leaving the other senders intact.""" pipe = PipelineBase() @@ -384,11 +415,11 @@ def test_remove_component_multiple_connections_to_same_neighbor(self): def test_remove_component_allows_you_to_reuse_the_component(self): pipe = PipelineBase() - Some = component_class("Some", input_types={"in": int}, output_types={"out": int}) + some = component_class("Some", input_types={"in": int}, output_types={"out": int}) - pipe.add_component("component_1", Some()) - pipe.add_component("component_2", Some()) - pipe.add_component("component_3", Some()) + pipe.add_component("component_1", some()) + pipe.add_component("component_2", some()) + pipe.add_component("component_3", some()) pipe.connect("component_1", "component_2") pipe.connect("component_2", "component_3") component_2 = pipe.remove_component("component_2") @@ -402,9 +433,9 @@ def test_remove_component_allows_you_to_reuse_the_component(self): } pipe2 = PipelineBase() - pipe2.add_component("component_4", Some()) + pipe2.add_component("component_4", some()) pipe2.add_component("component_2", component_2) - pipe2.add_component("component_5", Some()) + pipe2.add_component("component_5", some()) pipe2.connect("component_4", "component_2") pipe2.connect("component_2", "component_5") From e6786878a5f698007f04c0ac8f0f583c35a90fb6 Mon Sep 17 00:00:00 2001 From: "David S. Batista" Date: Wed, 22 Jul 2026 14:56:25 +0200 Subject: [PATCH 6/6] simplifying tests --- test/core/pipeline/test_pipeline_base.py | 56 +++++------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/test/core/pipeline/test_pipeline_base.py b/test/core/pipeline/test_pipeline_base.py index 52e10567514..6387df0b534 100644 --- a/test/core/pipeline/test_pipeline_base.py +++ b/test/core/pipeline/test_pipeline_base.py @@ -254,59 +254,30 @@ def test_remove_component_removes_component_and_its_edges(self): component_2 = component_class("Type2")() component_3 = component_class("Type3")() component_4 = component_class("Type4")() + isolated = component_class("Isolated")() pipe.add_component("1", component_1) pipe.add_component("2", component_2) pipe.add_component("3", component_3) pipe.add_component("4", component_4) + pipe.add_component("isolated", isolated) pipe.connect("1", "2") pipe.connect("2", "3") pipe.connect("3", "4") pipe.remove_component("2") + # Removing a component with no connections at all is a safe no-op over its (empty) edge sets. + removed_isolated = pipe.remove_component("isolated") assert sorted(pipe.graph.nodes) == ["1", "3", "4"] assert sorted([(u, v) for (u, v) in pipe.graph.edges()]) == [("3", "4")] - - def test_remove_component_clears_stale_senders_on_downstream_neighbor(self): - pipe = PipelineBase() - producer_class = component_class("Producer", output_types={"value": int}) - consumer_class = component_class("Consumer", input_types={"value": int}) - pipe.add_component("producer", producer_class()) - pipe.add_component("consumer", consumer_class()) - pipe.connect("producer.value", "consumer.value") - - pipe.remove_component("producer") - - consumer = pipe.get_component("consumer") - consumer_socket = consumer.__haystack_input__._sockets_dict["value"] # type: ignore[attr-defined] - # The surviving neighbor must not keep a dangling reference to the removed component. - assert consumer_socket.senders == [] - # With the stale sender gone, the now-unconnected mandatory input is exposed again. - assert pipe.inputs() == {"consumer": {"value": {"type": int, "is_mandatory": True}}} - # Feeding it directly must not raise a spurious "already connected" error. - pipe.validate_input({"consumer": {"value": 5}}) - - def test_remove_component_clears_stale_receivers_on_upstream_neighbor(self): - pipe = PipelineBase() - producer_class = component_class("Producer", output_types={"value": int}) - consumer_class = component_class("Consumer", input_types={"value": int}) - pipe.add_component("producer", producer_class()) - pipe.add_component("consumer", consumer_class()) - pipe.connect("producer.value", "consumer.value") - - pipe.remove_component("consumer") - - producer = pipe.get_component("producer") - producer_socket = producer.__haystack_output__._sockets_dict["value"] # type: ignore[attr-defined] - # The surviving upstream neighbor must not keep a dangling receiver reference. - assert producer_socket.receivers == [] + assert removed_isolated.__haystack_added_to_pipeline__ is None # type: ignore[attr-defined] def test_remove_component_middle_of_chain_removal(self): """Removing a component that is both a downstream and an upstream neighbor cleans up both sides.""" pipe = PipelineBase() - producer_class = component_class("Producer", output_types={"value": int}) + producer_class = component_class("Producer", input_types={}, output_types={"value": int}) middle_class = component_class("Middle", input_types={"value": int}, output_types={"value": int}) consumer_class = component_class("Consumer", input_types={"value": int}) pipe.add_component("producer", producer_class()) @@ -322,6 +293,10 @@ def test_remove_component_middle_of_chain_removal(self): # Both the upstream and the downstream neighbor must be cleaned of the removed component. assert producer_socket.receivers == [] assert consumer_socket.senders == [] + # With the stale sender gone, the now-unconnected mandatory input is exposed again. + assert pipe.inputs() == {"consumer": {"value": {"type": int, "is_mandatory": True}}} + # Feeding it directly must not raise a spurious "already connected" error. + pipe.validate_input({"consumer": {"value": 5}}) def test_remove_component_fan_out(self): """Removing a sender that fans out to multiple receivers cleans up every receiver, not just one.""" @@ -363,17 +338,6 @@ def test_remove_component_fan_in(self): assert producer_a_socket.receivers == [] assert producer_b_socket.receivers == [] - def test_remove_component_with_no_connections(self): - """Removing a component with no connections at all is a safe no-op over its (empty) edge sets.""" - pipe = PipelineBase() - some_class = component_class("Some", input_types={"in": int}, output_types={"out": int}) - pipe.add_component("isolated", some_class()) - - removed = pipe.remove_component("isolated") - - assert "isolated" not in pipe.graph.nodes - assert removed.__haystack_added_to_pipeline__ is None # type: ignore[attr-defined] - def test_remove_component_variadic_multi_sender(self): """Removing one sender of a variadic receiver only strips itself, leaving the other senders intact.""" pipe = PipelineBase()