@@ -2455,33 +2455,99 @@ mod tests {
24552455 ) ;
24562456 }
24572457
2458+ /// Exercises the combined auto-approve + state-transition logic from
2459+ /// `process_approval()`. This helper mirrors the single-lock pattern
2460+ /// at lines 1035-1064: auto-approve the tool, then attempt the state
2461+ /// transition, rolling back the auto-approve if the thread is gone.
2462+ ///
2463+ /// Returns `true` if the transition succeeded, `false` if the thread
2464+ /// was missing and the auto-approve was rolled back.
2465+ fn apply_auto_approve_and_transition (
2466+ session : & mut crate :: agent:: session:: Session ,
2467+ thread_id : uuid:: Uuid ,
2468+ tool_name : & str ,
2469+ always : bool ,
2470+ ) -> bool {
2471+ // Mirror the production code: auto-approve first, then transition.
2472+ if always {
2473+ session. auto_approve_tool ( tool_name) ;
2474+ }
2475+
2476+ match session. threads . get_mut ( & thread_id) {
2477+ Some ( thread) => {
2478+ thread. state = ThreadState :: Processing ;
2479+ true
2480+ }
2481+ None => {
2482+ // Roll back auto-approve if the thread vanished
2483+ if always {
2484+ session. auto_approved_tools . remove ( tool_name) ;
2485+ }
2486+ false
2487+ }
2488+ }
2489+ }
2490+
24582491 #[ test]
24592492 fn test_auto_approve_with_thread_disappearance_rolls_back ( ) {
2460- // Regression test: when always=true and the thread disappears after
2461- // auto_approve_tool is called, the auto-approve must be rolled back
2462- // to prevent a dangling policy for a tool that never executed.
2493+ // Regression test: when always=true and the thread disappears between
2494+ // the auto-approve and the state transition, the auto-approve must be
2495+ // rolled back to prevent a dangling policy for a tool that never
2496+ // executed. This mirrors the single-lock pattern in process_approval().
24632497 use crate :: agent:: session:: Session ;
2464- use std:: collections:: HashSet ;
2465- use uuid:: Uuid ;
24662498
24672499 let mut session = Session :: new ( "test-user" ) ;
2468- let thread_id = Uuid :: new_v4 ( ) ;
2469-
2470- // Simulate: always=true, auto-approve is set, but thread is missing
2471- session. auto_approve_tool ( "dangerous_tool" ) ;
2472- assert ! ( session. is_tool_auto_approved( "dangerous_tool" ) ) ;
2500+ let thread_id = uuid:: Uuid :: new_v4 ( ) ;
24732501
2474- // Thread doesn't exist — rollback should remove the auto-approve
2502+ // Thread does not exist — the combined operation must roll back
24752503 assert ! ( !session. threads. contains_key( & thread_id) ) ;
2476- // Simulate the rollback logic from process_approval
2477- session. auto_approved_tools . remove ( "dangerous_tool" ) ;
2478- assert ! ( !session. is_tool_auto_approved( "dangerous_tool" ) ) ;
2504+ let ok = apply_auto_approve_and_transition ( & mut session, thread_id, "dangerous_tool" , true ) ;
2505+ assert ! ( !ok, "transition must fail when thread is missing" ) ;
2506+ assert ! (
2507+ !session. is_tool_auto_approved( "dangerous_tool" ) ,
2508+ "auto-approve must be rolled back when the thread is missing"
2509+ ) ;
2510+ }
2511+
2512+ #[ test]
2513+ fn test_auto_approve_with_present_thread_succeeds ( ) {
2514+ // Happy-path counterpart: when always=true and the thread exists, the
2515+ // tool should remain auto-approved and the thread should transition
2516+ // to Processing.
2517+ use crate :: agent:: session:: Session ;
2518+
2519+ let mut session = Session :: new ( "test-user" ) ;
2520+ let thread = session. create_thread ( ) ;
2521+ let thread_id = thread. id ;
2522+
2523+ let ok = apply_auto_approve_and_transition ( & mut session, thread_id, "safe_tool" , true ) ;
2524+ assert ! ( ok, "transition must succeed when thread is present" ) ;
2525+ assert ! (
2526+ session. is_tool_auto_approved( "safe_tool" ) ,
2527+ "tool must remain auto-approved after successful transition"
2528+ ) ;
24792529 assert_eq ! (
2480- session
2481- . auto_approved_tools
2482- . intersection( & HashSet :: from( [ "dangerous_tool" . to_string( ) ] ) )
2483- . count( ) ,
2484- 0
2530+ session. threads. get( & thread_id) . map( |t| & t. state) ,
2531+ Some ( & ThreadState :: Processing ) ,
2532+ "thread must be in Processing state after approval"
2533+ ) ;
2534+ }
2535+
2536+ #[ test]
2537+ fn test_non_always_approve_does_not_add_to_auto_approved ( ) {
2538+ // When always=false, the tool should not be added to auto-approved
2539+ // regardless of whether the thread exists.
2540+ use crate :: agent:: session:: Session ;
2541+
2542+ let mut session = Session :: new ( "test-user" ) ;
2543+ let thread = session. create_thread ( ) ;
2544+ let thread_id = thread. id ;
2545+
2546+ let ok = apply_auto_approve_and_transition ( & mut session, thread_id, "one_time_tool" , false ) ;
2547+ assert ! ( ok, "transition must succeed when thread is present" ) ;
2548+ assert ! (
2549+ !session. is_tool_auto_approved( "one_time_tool" ) ,
2550+ "tool must not be auto-approved when always=false"
24852551 ) ;
24862552 }
24872553
0 commit comments