@@ -122,6 +122,34 @@ impl Endpoint for ExecAdapter {
122122 eprintln ! ( "runok: error: {}" , error) ;
123123 1
124124 }
125+
126+ fn handle_dry_run ( & self , result : ActionResult ) -> Result < i32 , anyhow:: Error > {
127+ match & result. action {
128+ Action :: Allow => {
129+ eprintln ! ( "runok: dry-run: command would be allowed" ) ;
130+ }
131+ Action :: Deny ( deny_response) => {
132+ let msg = deny_response
133+ . message
134+ . as_deref ( )
135+ . unwrap_or ( "command would be denied" ) ;
136+ eprintln ! ( "runok: dry-run: {}" , msg) ;
137+ if let Some ( suggestion) = & deny_response. fix_suggestion {
138+ eprintln ! ( "runok: dry-run: suggestion: {}" , suggestion) ;
139+ }
140+ }
141+ Action :: Ask ( message) => {
142+ let msg = message
143+ . as_deref ( )
144+ . unwrap_or ( "command would require confirmation" ) ;
145+ eprintln ! ( "runok: dry-run: {}" , msg) ;
146+ }
147+ Action :: Default => {
148+ eprintln ! ( "runok: dry-run: no matching rule (default behavior)" ) ;
149+ }
150+ }
151+ Ok ( 0 )
152+ }
125153}
126154
127155#[ cfg( test) ]
@@ -498,4 +526,63 @@ mod tests {
498526 let captured = executed_command. lock ( ) . unwrap ( ) ;
499527 assert_eq ! ( * captured, Some ( expected) ) ;
500528 }
529+
530+ // --- handle_dry_run ---
531+
532+ #[ rstest]
533+ #[ case:: allow( Action :: Allow , 0 ) ]
534+ #[ case:: deny(
535+ Action :: Deny ( DenyResponse {
536+ message: Some ( "dangerous" . to_string( ) ) ,
537+ fix_suggestion: Some ( "use safer command" . to_string( ) ) ,
538+ matched_rule: "rm *" . to_string( ) ,
539+ } ) ,
540+ 0
541+ ) ]
542+ #[ case:: ask(
543+ Action :: Ask ( Some ( "please confirm" . to_string( ) ) ) ,
544+ 0
545+ ) ]
546+ #[ case:: default_action( Action :: Default , 0 ) ]
547+ fn handle_dry_run_always_returns_exit_0 (
548+ #[ case] action : Action ,
549+ #[ case] expected_exit_code : i32 ,
550+ ) {
551+ let adapter = ExecAdapter :: new (
552+ vec ! [ "git" . into( ) , "status" . into( ) ] ,
553+ None ,
554+ Box :: new ( MockExecutor :: new ( 42 ) ) ,
555+ ) ;
556+ let result = adapter
557+ . handle_dry_run ( ActionResult {
558+ action,
559+ sandbox : SandboxInfo :: Preset ( None ) ,
560+ } )
561+ . unwrap ( ) ;
562+ assert_eq ! ( result, expected_exit_code) ;
563+ }
564+
565+ #[ rstest]
566+ fn handle_dry_run_does_not_execute_command ( ) {
567+ let captured: std:: sync:: Arc < std:: sync:: Mutex < Option < CommandInput > > > =
568+ std:: sync:: Arc :: new ( std:: sync:: Mutex :: new ( None ) ) ;
569+
570+ let adapter = ExecAdapter :: new (
571+ vec ! [ "git" . into( ) , "status" . into( ) ] ,
572+ None ,
573+ Box :: new ( CapturingExecutor {
574+ captured : captured. clone ( ) ,
575+ } ) ,
576+ ) ;
577+
578+ adapter
579+ . handle_dry_run ( ActionResult {
580+ action : Action :: Allow ,
581+ sandbox : SandboxInfo :: Preset ( None ) ,
582+ } )
583+ . unwrap ( ) ;
584+
585+ // Command should NOT be executed in dry-run mode
586+ assert ! ( captured. lock( ) . unwrap( ) . is_none( ) ) ;
587+ }
501588}
0 commit comments