# Copyright 2026 Google LLC # # 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 _BranchPath. Verifies that _BranchPath correctly parses, serializes, and manipulates hierarchical dynamic execution branch paths. """ from __future__ import annotations from google.adk.events._branch_path import _BranchPath import pytest def test_from_string_with_empty_string_returns_empty_path(): """Parsing an empty string returns a _BranchPath with no segments.""" path = _BranchPath.from_string("") assert path.segments == [] assert str(path) == "" def test_from_string_with_single_segment_returns_path_with_one_segment(): """Parsing a single name returns a _BranchPath with one segment.""" path = _BranchPath.from_string("agent_0") assert path.segments == ["agent_0"] assert str(path) == "agent_0" def test_from_string_with_multiple_segments_returns_path_with_all_segments(): """Parsing a dot-separated string returns a _BranchPath with all segments.""" path = _BranchPath.from_string("parent.child.node") assert path.segments == ["parent", "child", "node"] assert str(path) == "parent.child.node" def test_equality_compares_path_segments(): """Two _BranchPath objects are equal if and only if their segments match.""" path1 = _BranchPath.from_string("parent.child") path2 = _BranchPath.from_string("parent.child") path3 = _BranchPath.from_string("parent.other") assert path1 == path2 assert path1 != path3 assert path1 != "parent.child" # Different type def test_run_ids_extracts_all_run_ids_from_path(): """run_ids extracts all run IDs (the part after '@') from all segments.""" # Given paths with various run ID patterns path_with_ids = _BranchPath.from_string("parent@1.child@2.node") path_no_ids = _BranchPath.from_string("parent.child") path_mixed = _BranchPath.from_string("parent@1.child.node@3") # Then the extracted run IDs match expectations assert path_with_ids.run_ids == {"1", "2"} assert path_no_ids.run_ids == set() assert path_mixed.run_ids == {"1", "3"} def test_parent_returns_parent_path_or_none_for_root(): """parent returns a new _BranchPath excluding the leaf segment, or None.""" path = _BranchPath.from_string("parent.child.node") assert path.parent == _BranchPath.from_string("parent.child") assert path.parent.parent == _BranchPath.from_string("parent") assert path.parent.parent.parent is None def test_is_descendant_of_verifies_path_hierarchy_safely(): """is_descendant_of returns True if the path is a strict sub-path of ancestor.""" # Given an ancestor and various comparison paths ancestor = _BranchPath.from_string("parent.child") descendant = _BranchPath.from_string("parent.child.node.leaf") not_descendant = _BranchPath.from_string("parent.other") same = _BranchPath.from_string("parent.child") # Then descendant checks match expectations assert descendant.is_descendant_of(ancestor) assert not ancestor.is_descendant_of(descendant) assert not not_descendant.is_descendant_of(ancestor) assert not same.is_descendant_of(ancestor) def test_is_descendant_of_is_immune_to_partial_name_prefix_match(): """is_descendant_of compares segments, avoiding partial string prefix bugs.""" # Given an ancestor and a path that has a partial string prefix but different segment ancestor = _BranchPath.from_string("agent_0") descendant_with_prefix = _BranchPath.from_string("agent_00.child") # Then it is not recognized as a descendant because segments don't match assert not descendant_with_prefix.is_descendant_of(ancestor) def test_common_prefix_finds_longest_shared_path(): """common_prefix returns the longest common prefix of a list of paths.""" # Given a list of paths sharing a common prefix paths = [ _BranchPath.from_string("parent.child.node1"), _BranchPath.from_string("parent.child.node2.leaf"), _BranchPath.from_string("parent.child.node3"), ] # When finding the common prefix result = _BranchPath.common_prefix(paths) # Then the result matches the shared parent path assert result == _BranchPath.from_string("parent.child") def test_common_prefix_with_no_shared_path_returns_empty(): """common_prefix returns an empty path if there is no shared prefix.""" paths = [ _BranchPath.from_string("parent.child"), _BranchPath.from_string("other.child"), ] result = _BranchPath.common_prefix(paths) assert result == _BranchPath.from_string("") def test_common_prefix_with_empty_list_returns_empty(): """common_prefix returns an empty path if the input list is empty.""" result = _BranchPath.common_prefix([]) assert result == _BranchPath.from_string("") def test_constructor_copies_segments_list(): """_BranchPath copies the input segments list to ensure immutability.""" segments = ["parent", "child"] path = _BranchPath(segments) # Mutate the original list segments.append("grandchild") # The path segments should remain unchanged assert path.segments == ["parent", "child"] def test_append_single_segment_returns_new_path(): """append adds a single segment to an existing path.""" path = _BranchPath.from_string("parent") new_path = path.append("child") assert new_path == _BranchPath.from_string("parent.child") assert path == _BranchPath.from_string("parent") # Immutability check def test_append_with_run_id_formats_segment(): """append formats the segment as 'name@run_id' when run_id is provided.""" path = _BranchPath.from_string("parent") new_path = path.append("child", run_id="call_123") assert new_path == _BranchPath.from_string("parent.child@call_123") def test_append_another_branch_path(): """append combines segments from another _BranchPath instance.""" path1 = _BranchPath.from_string("parent") path2 = _BranchPath.from_string("child.grandchild") new_path = path1.append(path2) assert new_path == _BranchPath.from_string("parent.child.grandchild") def test_create_sub_branch_formats_string_correctly(): """create_sub_branch constructs sub-branch strings safely.""" # With base branch and run ID res1 = _BranchPath.create_sub_branch( "parent.sub", name="child", run_id="run_1" ) assert res1 == "parent.sub.child@run_1" # Without base branch (None or empty) res2 = _BranchPath.create_sub_branch(None, name="agent", run_id="fc_456") assert res2 == "agent@fc_456" # Dot-separated sub-path without run ID res3 = _BranchPath.create_sub_branch("parent", name="agent.sub_agent") assert res3 == "parent.agent.sub_agent" def test_append_with_run_id_and_branch_path_raises_value_error(): """append raises ValueError when run_id is provided with a _BranchPath.""" path1 = _BranchPath.from_string("parent") path2 = _BranchPath.from_string("child") with pytest.raises(ValueError, match="run_id cannot be provided"): path1.append(path2, run_id="123") def test_append_with_run_id_and_dot_separated_path_raises_value_error(): """append raises ValueError when run_id is provided with a dot-separated path.""" path = _BranchPath.from_string("parent") with pytest.raises(ValueError, match="run_id cannot be provided"): path.append("child.sub", run_id="123") def test_run_ids_filters_out_empty_run_ids(): """run_ids filters out segments with empty run IDs (e.g. ending with '@').""" path = _BranchPath.from_string("parent@.child@2.node@") assert path.run_ids == {"2"}