1717from typing import Any
1818
1919from predicate .agents .planner_executor_agent import (
20+ AuthBoundaryConfig ,
2021 ExecutorOverride ,
2122 IntentHeuristics ,
2223 ModalDismissalConfig ,
@@ -687,6 +688,14 @@ def test_custom_scroll_settings(self) -> None:
687688 assert config .scroll_max_attempts == 5
688689 assert config .scroll_directions == ("down" ,)
689690
691+ def test_scroll_viewport_fraction_default (self ) -> None :
692+ config = SnapshotEscalationConfig ()
693+ assert config .scroll_viewport_fraction == 0.4
694+
695+ def test_scroll_viewport_fraction_custom (self ) -> None :
696+ config = SnapshotEscalationConfig (scroll_viewport_fraction = 0.5 )
697+ assert config .scroll_viewport_fraction == 0.5
698+
690699 def test_scroll_directions_can_be_reordered (self ) -> None :
691700 # Try up first, then down
692701 config = SnapshotEscalationConfig (
@@ -788,13 +797,26 @@ def __init__(self, elements: list[MockElement], url: str = "https://example.com"
788797
789798
790799class MockRuntime :
791- """Mock runtime for testing scroll-after-escalation."""
800+ """Mock runtime for testing scroll-after-escalation and auth boundary ."""
792801
793- def __init__ (self , snapshots_by_scroll : dict [int , MockSnapshot ] | None = None ):
802+ def __init__ (
803+ self ,
804+ snapshots_by_scroll : dict [int , MockSnapshot ] | None = None ,
805+ url : str = "https://example.com" ,
806+ ):
794807 self .scroll_count = 0
795808 self .scroll_directions : list [str ] = []
796809 self .snapshots_by_scroll = snapshots_by_scroll or {}
797810 self .default_snapshot = MockSnapshot ([MockElement (1 , "button" , "Submit" )])
811+ self ._url = url
812+
813+ async def get_url (self ) -> str :
814+ """Return the current URL for auth boundary detection."""
815+ return self ._url
816+
817+ async def get_viewport_height (self ) -> int :
818+ """Return mock viewport height."""
819+ return 800 # Standard viewport height
798820
799821 async def snapshot (
800822 self ,
@@ -810,6 +832,23 @@ async def scroll(self, direction: str = "down") -> None:
810832 self .scroll_count += 1
811833 self .scroll_directions .append (direction )
812834
835+ async def scroll_by (
836+ self ,
837+ dy : float ,
838+ * ,
839+ verify : bool = True ,
840+ min_delta_px : float = 50.0 ,
841+ js_fallback : bool = True ,
842+ required : bool = True ,
843+ timeout_s : float = 10.0 ,
844+ ** kwargs : Any ,
845+ ) -> bool :
846+ """Mock scroll_by with verification - always returns True (scroll effective)."""
847+ direction = "down" if dy > 0 else "up"
848+ self .scroll_count += 1
849+ self .scroll_directions .append (direction )
850+ return True # Scroll always effective in tests
851+
813852 async def stabilize (self ) -> None :
814853 pass
815854
@@ -1136,3 +1175,74 @@ async def test_no_scroll_without_intent_heuristics(self) -> None:
11361175
11371176 # No scrolling should occur without intent_heuristics
11381177 assert runtime .scroll_count == 0
1178+
1179+
1180+ # ---------------------------------------------------------------------------
1181+ # Test AuthBoundaryConfig
1182+ # ---------------------------------------------------------------------------
1183+
1184+
1185+ class TestAuthBoundaryConfig :
1186+ """Tests for AuthBoundaryConfig."""
1187+
1188+ def test_default_values (self ) -> None :
1189+ config = AuthBoundaryConfig ()
1190+ assert config .enabled is True
1191+ assert config .stop_on_auth is True
1192+ assert "/signin" in config .url_patterns
1193+ assert "/ap/signin" in config .url_patterns
1194+ assert "/ax/claim" in config .url_patterns # Amazon CAPTCHA
1195+
1196+ def test_default_url_patterns_include_common_patterns (self ) -> None :
1197+ config = AuthBoundaryConfig ()
1198+ expected_patterns = [
1199+ "/signin" ,
1200+ "/sign-in" ,
1201+ "/login" ,
1202+ "/log-in" ,
1203+ "/auth" ,
1204+ "/authenticate" ,
1205+ "/ap/signin" , # Amazon
1206+ "/ap/register" , # Amazon
1207+ "/ax/claim" , # Amazon CAPTCHA
1208+ "/account/login" ,
1209+ "/accounts/login" ,
1210+ "/user/login" ,
1211+ ]
1212+ for pattern in expected_patterns :
1213+ assert pattern in config .url_patterns , f"Missing pattern: { pattern } "
1214+
1215+ def test_can_be_disabled (self ) -> None :
1216+ config = AuthBoundaryConfig (enabled = False )
1217+ assert config .enabled is False
1218+
1219+ def test_stop_on_auth_can_be_disabled (self ) -> None :
1220+ config = AuthBoundaryConfig (stop_on_auth = False )
1221+ assert config .stop_on_auth is False
1222+
1223+ def test_custom_url_patterns (self ) -> None :
1224+ config = AuthBoundaryConfig (
1225+ url_patterns = ("/custom/login" , "/my-signin" ),
1226+ )
1227+ assert config .url_patterns == ("/custom/login" , "/my-signin" )
1228+
1229+ def test_custom_auth_success_message (self ) -> None :
1230+ config = AuthBoundaryConfig (
1231+ auth_success_message = "Custom: Login required" ,
1232+ )
1233+ assert config .auth_success_message == "Custom: Login required"
1234+
1235+ def test_planner_executor_config_has_auth_boundary (self ) -> None :
1236+ config = PlannerExecutorConfig ()
1237+ assert config .auth_boundary is not None
1238+ assert config .auth_boundary .enabled is True
1239+
1240+ def test_planner_executor_config_custom_auth_boundary (self ) -> None :
1241+ config = PlannerExecutorConfig (
1242+ auth_boundary = AuthBoundaryConfig (
1243+ enabled = True ,
1244+ url_patterns = ("/custom-signin" ,),
1245+ stop_on_auth = True ,
1246+ ),
1247+ )
1248+ assert config .auth_boundary .url_patterns == ("/custom-signin" ,)
0 commit comments