44
55import json
66
7- from pythinker_code .soul .approval import Approval , ApprovalState
7+ from pythinker_code .soul .approval import Approval , ApprovalState , deliberation_scope
88from pythinker_code .wire .types import ToolCall
99
1010
@@ -15,6 +15,39 @@ def _shell_call(cmd: str) -> ToolCall:
1515 )
1616
1717
18+ def test_tool_destructive_reason_gates_background_shell () -> None :
19+ from pythinker_code .soul .permission import tool_destructive_reason
20+
21+ # Background shell is the same "Shell" tool (run_in_background=true); a destructive
22+ # background command must still be classified as destructive.
23+ reason = tool_destructive_reason (
24+ "Shell" , {"command" : "rm -rf build" , "run_in_background" : True }
25+ )
26+ assert reason is not None
27+
28+
29+ def test_tool_destructive_reason_ignores_unregistered_tool () -> None :
30+ from pythinker_code .soul .permission import tool_destructive_reason
31+
32+ assert (
33+ tool_destructive_reason ("WriteFile" , {"path" : "x" , "content" : "y" , "mode" : "overwrite" })
34+ is None
35+ )
36+
37+
38+ def test_deliberation_scope_sets_and_restores_contextvar () -> None :
39+ from pythinker_code .soul .approval import (
40+ DeliberationScope ,
41+ _current_deliberation_scope ,
42+ deliberation_scope ,
43+ )
44+
45+ assert _current_deliberation_scope .get () is None
46+ with deliberation_scope ("root" , 3 ):
47+ assert _current_deliberation_scope .get () == DeliberationScope ("root" , 3 )
48+ assert _current_deliberation_scope .get () is None
49+
50+
1851def test_yolo_only () -> None :
1952 approval = Approval (yolo = True )
2053 assert approval .is_yolo () is True
@@ -117,19 +150,43 @@ def test_set_auto_false_clears_runtime_auto() -> None:
117150
118151
119152def test_destructive_action_deliberates_once_then_proceeds_under_auto () -> None :
120- """auto + auto_deliberate: a destructive Shell command deliberates the first
121- time, the identical re-issue runs once (one-shot retry), and a third issue
122- deliberates again — so deliberation never permanently whitelists ``rm -rf``."""
153+ """auto + auto_deliberate: a destructive command deliberates the first time, the
154+ re-issue in a LATER generation runs once, and a fresh issue later deliberates again."""
123155 approval = Approval (state = ApprovalState (auto = True , auto_deliberate = True ))
156+ with deliberation_scope ("root" , 1 ):
157+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
158+ with deliberation_scope ("root" , 2 ):
159+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is None
160+ with deliberation_scope ("root" , 3 ):
161+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
124162
125- first = approval .deliberation_gate (_shell_call ("rm -rf build" ))
126- assert first is not None , "first destructive issue should deliberate"
127163
128- second = approval .deliberation_gate (_shell_call ("rm -rf build" ))
129- assert second is None , "identical re-issue is the one-shot retry: allowed through"
164+ def test_same_generation_duplicate_destructive_calls_both_bounce () -> None :
165+ # Property (a): two byte-identical destructive calls in ONE generation both deliberate.
166+ approval = Approval (state = ApprovalState (auto = True , auto_deliberate = True ))
167+ with deliberation_scope ("root" , 1 ):
168+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
169+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
170+
130171
131- third = approval .deliberation_gate (_shell_call ("rm -rf build" ))
132- assert third is not None , "one-shot consumed; a fresh issue deliberates again"
172+ def test_subagent_identical_call_does_not_consume_main_one_shot () -> None :
173+ # Property (c): a subagent's identical call must not ride on the main agent's bounce.
174+ approval = Approval (state = ApprovalState (auto = True , auto_deliberate = True ))
175+ with deliberation_scope ("root" , 1 ):
176+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
177+ with deliberation_scope ("sub-1" , 1 ):
178+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
179+
180+
181+ def test_older_generation_duplicate_destructive_call_still_bounces () -> None :
182+ # Defensive guard: only a strictly later generation can consume a prior bounce.
183+ approval = Approval (state = ApprovalState (auto = True , auto_deliberate = True ))
184+ with deliberation_scope ("root" , 2 ):
185+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
186+ with deliberation_scope ("root" , 1 ):
187+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
188+ with deliberation_scope ("root" , 2 ):
189+ assert approval .deliberation_gate (_shell_call ("rm -rf build" )) is not None
133190
134191
135192def test_deliberation_gate_conditions () -> None :
@@ -164,14 +221,16 @@ async def test_request_bounces_destructive_then_approves_retry() -> None:
164221
165222 approval = Approval (state = ApprovalState (auto = True , auto_deliberate = True ))
166223 with tool_call_context ("Shell" , arguments = {"command" : "rm -rf build" }):
167- first = await approval .request ("Shell" , "run command" , "Run command `rm -rf build`" )
224+ with deliberation_scope ("root" , 1 ):
225+ first = await approval .request ("Shell" , "run command" , "Run command `rm -rf build`" )
168226 assert not first , "destructive action is bounced for deliberation"
169227 assert first .deliberation is True
170228 assert "irreversible" in first .feedback
171229 assert "rejected by the user" not in first .rejection_error ().message
172230
173- second = await approval .request ("Shell" , "run command" , "Run command `rm -rf build`" )
174- assert second , "one-shot consumed: the deliberated retry runs"
231+ with deliberation_scope ("root" , 2 ):
232+ second = await approval .request ("Shell" , "run command" , "Run command `rm -rf build`" )
233+ assert second , "one-shot consumed in a later generation: the deliberated retry runs"
175234
176235
177236def test_approval_state_honors_auto_deliberate_flag () -> None :
0 commit comments