@@ -54,6 +54,27 @@ async def test_starts_with_matches_prefix(self) -> None:
5454 assert result .matched is True
5555 assert result .metadata ["matches" ] == ["/home/lev/agent-control/server/src/app.py" ]
5656
57+ @pytest .mark .asyncio
58+ async def test_starts_with_matches_exact_path_value (self ) -> None :
59+ """Test that starts_with mode matches the configured path value exactly."""
60+ # Given: a starts_with evaluator config
61+ evaluator = ListEvaluator (
62+ ListEvaluatorConfig (
63+ values = ["/home/lev/agent-control" ],
64+ logic = "any" ,
65+ match_on = "match" ,
66+ match_mode = "starts_with" ,
67+ case_sensitive = True ,
68+ )
69+ )
70+
71+ # When: evaluating the exact configured path
72+ result = await evaluator .evaluate ("/home/lev/agent-control" )
73+
74+ # Then: the exact path matches
75+ assert result .matched is True
76+ assert result .metadata ["matches" ] == ["/home/lev/agent-control" ]
77+
5778 @pytest .mark .asyncio
5879 async def test_starts_with_no_match_when_prefix_absent (self ) -> None :
5980 """Test that starts_with mode does not trigger when no prefix matches."""
@@ -74,6 +95,27 @@ async def test_starts_with_no_match_when_prefix_absent(self) -> None:
7495 # Then: the evaluator does not trigger
7596 assert result .matched is False
7697
98+ @pytest .mark .asyncio
99+ async def test_starts_with_uses_raw_string_prefix_for_path_like_values (self ) -> None :
100+ """Test that starts_with is generic string-prefix matching, not path-segment aware."""
101+ # Given: a starts_with evaluator configured with a path-like prefix
102+ evaluator = ListEvaluator (
103+ ListEvaluatorConfig (
104+ values = ["/home/lev/agent-control" ],
105+ logic = "any" ,
106+ match_on = "match" ,
107+ match_mode = "starts_with" ,
108+ case_sensitive = True ,
109+ )
110+ )
111+
112+ # When: evaluating a sibling path that shares the same string prefix
113+ result = await evaluator .evaluate ("/home/lev/agent-control-old/server" )
114+
115+ # Then: the evaluator matches because starts_with is not path-boundary aware
116+ assert result .matched is True
117+ assert result .metadata ["matches" ] == ["/home/lev/agent-control-old/server" ]
118+
77119 @pytest .mark .asyncio
78120 async def test_starts_with_honors_case_sensitivity (self ) -> None :
79121 """Test that starts_with mode respects case sensitivity settings."""
@@ -127,6 +169,97 @@ async def test_starts_with_supports_no_match_allowlists(self) -> None:
127169 assert allowed_result .matched is False
128170 assert denied_result .matched is True
129171
172+ @pytest .mark .asyncio
173+ async def test_starts_with_matches_plain_text_prefix (self ) -> None :
174+ """Test that starts_with mode works for ordinary non-path strings."""
175+ # Given: a starts_with evaluator config for ordinary strings
176+ evaluator = ListEvaluator (
177+ ListEvaluatorConfig (
178+ values = ["agent" , "control:" ],
179+ logic = "any" ,
180+ match_on = "match" ,
181+ match_mode = "starts_with" ,
182+ case_sensitive = True ,
183+ )
184+ )
185+
186+ # When: evaluating values with and without the configured prefixes
187+ matched_result = await evaluator .evaluate ("agent-control" )
188+ unmatched_result = await evaluator .evaluate ("please control: now" )
189+
190+ # Then: only the true prefix match triggers
191+ assert matched_result .matched is True
192+ assert unmatched_result .matched is False
193+
194+ @pytest .mark .asyncio
195+ async def test_starts_with_escapes_regex_metacharacters (self ) -> None :
196+ """Test that starts_with treats configured values as literals, not regex."""
197+ # Given: prefixes containing regex metacharacters
198+ evaluator = ListEvaluator (
199+ ListEvaluatorConfig (
200+ values = ["release/v1.2+" , "[beta]" ],
201+ logic = "any" ,
202+ match_on = "match" ,
203+ match_mode = "starts_with" ,
204+ case_sensitive = True ,
205+ )
206+ )
207+
208+ # When: evaluating values that begin with those literal prefixes
209+ release_result = await evaluator .evaluate ("release/v1.2+rc1" )
210+ beta_result = await evaluator .evaluate ("[beta] feature flag" )
211+
212+ # Then: both values match literally
213+ assert release_result .matched is True
214+ assert beta_result .matched is True
215+
216+ @pytest .mark .asyncio
217+ async def test_starts_with_supports_list_input_with_any_logic (self ) -> None :
218+ """Test that starts_with works on list inputs when any item matches."""
219+ # Given: a starts_with evaluator with any-item semantics
220+ evaluator = ListEvaluator (
221+ ListEvaluatorConfig (
222+ values = ["/home/lev/agent-control" , "agent" ],
223+ logic = "any" ,
224+ match_on = "match" ,
225+ match_mode = "starts_with" ,
226+ case_sensitive = True ,
227+ )
228+ )
229+
230+ # When: evaluating a list where only one element matches
231+ result = await evaluator .evaluate (["/var/log/system.log" , "agent-control" ])
232+
233+ # Then: the evaluator triggers and reports the matching entry
234+ assert result .matched is True
235+ assert result .metadata ["matches" ] == ["agent-control" ]
236+
237+ @pytest .mark .asyncio
238+ async def test_starts_with_supports_list_input_with_all_logic (self ) -> None :
239+ """Test that starts_with respects all-item semantics for list inputs."""
240+ # Given: a starts_with evaluator with all-item semantics
241+ evaluator = ListEvaluator (
242+ ListEvaluatorConfig (
243+ values = ["/home/lev/agent-control" , "/tmp/cache" ],
244+ logic = "all" ,
245+ match_on = "match" ,
246+ match_mode = "starts_with" ,
247+ case_sensitive = True ,
248+ )
249+ )
250+
251+ # When: evaluating one fully matching list and one partially matching list
252+ matching_result = await evaluator .evaluate (
253+ ["/home/lev/agent-control/server" , "/tmp/cache/build" ]
254+ )
255+ partial_result = await evaluator .evaluate (
256+ ["/home/lev/agent-control/server" , "/var/log/system.log" ]
257+ )
258+
259+ # Then: only the fully matching list triggers
260+ assert matching_result .matched is True
261+ assert partial_result .matched is False
262+
130263 @pytest .mark .asyncio
131264 async def test_legacy_empty_string_value_is_ignored_defensively (self ) -> None :
132265 """Test that legacy invalid configs do not compile into a match-all regex."""
0 commit comments