22
33import pytest
44
5- from minicode .permissions import PermissionManager
5+ import minicode .permissions as permissions_module
6+ from minicode .permissions import PermissionManager , _classify_dangerous_command , _is_within_directory
7+
8+
9+ @pytest .fixture (autouse = True )
10+ def isolated_permission_store (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
11+ store_path = tmp_path / "home" / "permissions.json"
12+ monkeypatch .setattr (permissions_module , "MINI_CODE_PERMISSIONS_PATH" , store_path )
13+ permissions_module ._normalize_path_cached .cache_clear ()
14+ yield store_path
15+ permissions_module ._normalize_path_cached .cache_clear ()
616
717
818def test_permission_manager_uses_prompt_for_external_path (tmp_path : Path ) -> None :
@@ -16,3 +26,159 @@ def test_permission_manager_denies_external_path_without_prompt(tmp_path: Path)
1626 manager = PermissionManager (str (tmp_path ))
1727 with pytest .raises (RuntimeError ):
1828 manager .ensure_path_access (str (external ), "read" )
29+
30+
31+ def test_allow_always_persists_external_directory (tmp_path : Path ) -> None :
32+ workspace = tmp_path / "workspace"
33+ external_dir = tmp_path / "shared"
34+ workspace .mkdir ()
35+ external_dir .mkdir ()
36+ first = external_dir / "first.txt"
37+ second = external_dir / "second.txt"
38+
39+ manager = PermissionManager (
40+ str (workspace ),
41+ prompt = lambda request : {"decision" : "allow_always" },
42+ )
43+ manager .ensure_path_access (str (first ), "read" )
44+
45+ reloaded = PermissionManager (str (workspace ), prompt = None )
46+ reloaded .ensure_path_access (str (second ), "read" )
47+
48+
49+ def test_deny_always_persists_external_directory_and_wins (tmp_path : Path ) -> None :
50+ workspace = tmp_path / "workspace"
51+ external_dir = tmp_path / "shared"
52+ workspace .mkdir ()
53+ external_dir .mkdir ()
54+ target = external_dir / "blocked.txt"
55+
56+ manager = PermissionManager (
57+ str (workspace ),
58+ prompt = lambda request : {"decision" : "deny_always" },
59+ )
60+ with pytest .raises (RuntimeError , match = "Access denied" ):
61+ manager .ensure_path_access (str (target ), "read" )
62+
63+ reloaded = PermissionManager (
64+ str (workspace ),
65+ prompt = lambda request : pytest .fail ("persisted deny should not reprompt" ),
66+ )
67+ with pytest .raises (RuntimeError , match = "Access denied" ):
68+ reloaded .ensure_path_access (str (target ), "read" )
69+
70+
71+ def test_dangerous_command_allow_always_persists (tmp_path : Path ) -> None :
72+ workspace = tmp_path / "workspace"
73+ workspace .mkdir ()
74+
75+ manager = PermissionManager (
76+ str (workspace ),
77+ prompt = lambda request : {"decision" : "allow_always" },
78+ )
79+ manager .ensure_command ("git" , ["reset" , "--hard" ], str (workspace ))
80+
81+ reloaded = PermissionManager (
82+ str (workspace ),
83+ prompt = lambda request : pytest .fail ("persisted command allow should not reprompt" ),
84+ )
85+ reloaded .ensure_command ("git" , ["reset" , "--hard" ], str (workspace ))
86+
87+
88+ def test_dangerous_command_deny_always_persists (tmp_path : Path ) -> None :
89+ workspace = tmp_path / "workspace"
90+ workspace .mkdir ()
91+
92+ manager = PermissionManager (
93+ str (workspace ),
94+ prompt = lambda request : {"decision" : "deny_always" },
95+ )
96+ with pytest .raises (RuntimeError , match = "Command denied" ):
97+ manager .ensure_command ("git" , ["clean" , "-fd" ], str (workspace ))
98+
99+ reloaded = PermissionManager (
100+ str (workspace ),
101+ prompt = lambda request : pytest .fail ("persisted command deny should not reprompt" ),
102+ )
103+ with pytest .raises (RuntimeError , match = "Command denied" ):
104+ reloaded .ensure_command ("git" , ["clean" , "-fd" ], str (workspace ))
105+
106+
107+ def test_edit_deny_with_feedback_surfaces_guidance (tmp_path : Path ) -> None :
108+ target = tmp_path / "demo.py"
109+ target .write_text ("old\n " , encoding = "utf-8" )
110+ manager = PermissionManager (
111+ str (tmp_path ),
112+ prompt = lambda request : {
113+ "decision" : "deny_with_feedback" ,
114+ "feedback" : "Please keep this file stable." ,
115+ },
116+ )
117+
118+ with pytest .raises (RuntimeError , match = "Please keep this file stable" ):
119+ manager .ensure_edit (str (target ), "- old\n + new\n " )
120+
121+
122+ def test_turn_scoped_edit_permissions_reset_between_turns (tmp_path : Path ) -> None :
123+ target = tmp_path / "demo.py"
124+ target .write_text ("old\n " , encoding = "utf-8" )
125+ prompts : list [dict ] = []
126+
127+ def prompt (request : dict ):
128+ prompts .append (request )
129+ return {"decision" : "allow_turn" }
130+
131+ manager = PermissionManager (str (tmp_path ), prompt = prompt )
132+ manager .begin_turn ()
133+ manager .ensure_edit (str (target ), "- old\n + new\n " )
134+ manager .ensure_edit (str (target ), "- old\n + newer\n " )
135+ assert len (prompts ) == 1
136+
137+ manager .end_turn ()
138+ manager .ensure_edit (str (target ), "- old\n + newest\n " )
139+ assert len (prompts ) == 2
140+
141+
142+ def test_allow_all_turn_applies_to_multiple_files_for_one_turn (tmp_path : Path ) -> None :
143+ first = tmp_path / "first.py"
144+ second = tmp_path / "second.py"
145+ first .write_text ("old\n " , encoding = "utf-8" )
146+ second .write_text ("old\n " , encoding = "utf-8" )
147+ prompts : list [dict ] = []
148+
149+ def prompt (request : dict ):
150+ prompts .append (request )
151+ return {"decision" : "allow_all_turn" }
152+
153+ manager = PermissionManager (str (tmp_path ), prompt = prompt )
154+ manager .begin_turn ()
155+ manager .ensure_edit (str (first ), "- old\n + new\n " )
156+ manager .ensure_edit (str (second ), "- old\n + new\n " )
157+ assert len (prompts ) == 1
158+
159+ manager .end_turn ()
160+ manager .ensure_edit (str (first ), "- old\n + newest\n " )
161+ assert len (prompts ) == 2
162+
163+
164+ @pytest .mark .parametrize (
165+ ("command" , "args" , "expected" ),
166+ [
167+ ("git" , ["reset" , "--hard" ], "discard local changes" ),
168+ ("git" , ["push" , "--force" ], "rewrites remote history" ),
169+ ("rm" , ["-rf" , "build" ], "catastrophic data loss" ),
170+ ("python" , ["script.py" ], "arbitrary local code" ),
171+ ("chmod" , ["777" , "file" ], "opens permissions" ),
172+ ],
173+ )
174+ def test_destructive_command_classification (command : str , args : list [str ], expected : str ) -> None :
175+ reason = _classify_dangerous_command (command , args )
176+
177+ assert reason is not None
178+ assert expected in reason
179+
180+
181+ def test_windows_style_directory_match_is_case_insensitive (monkeypatch : pytest .MonkeyPatch ) -> None :
182+ monkeypatch .setattr (permissions_module , "_is_win" , True )
183+
184+ assert _is_within_directory ("/Users/Alice/Repo" , "/users/alice/repo/src/main.py" )
0 commit comments