@@ -60,6 +60,9 @@ struct MockState {
6060 last_workspace_request : Mutex < Option < String > > ,
6161 get_calls : AtomicU32 ,
6262 phase_sequence : Vec < proto:: SandboxPhase > ,
63+ get_sandbox_id : Option < String > ,
64+ get_error : Option < Status > ,
65+ delete_response : Option < proto:: DeleteSandboxResponse > ,
6366 get_returns_not_found : bool ,
6467 not_found_after : Option < u32 > ,
6568 paginate_list : bool ,
@@ -337,6 +340,9 @@ impl OpenShell for TestOpenShell {
337340 selected_workspace ( & req. workspace_scope ) . map ( str:: to_string) ;
338341 let count = self . state . get_calls . fetch_add ( 1 , Ordering :: SeqCst ) ;
339342
343+ if let Some ( error) = & self . state . get_error {
344+ return Err ( error. clone ( ) ) ;
345+ }
340346 if self . state . get_returns_not_found {
341347 return Err ( Status :: not_found ( format ! ( "sandbox '{name}' not found" ) ) ) ;
342348 }
@@ -354,8 +360,12 @@ impl OpenShell for TestOpenShell {
354360 . or_else ( || self . state . phase_sequence . last ( ) . copied ( ) )
355361 . unwrap_or ( proto:: SandboxPhase :: Ready ) ;
356362
363+ let mut sandbox = sandbox_with_phase ( & name, phase) ;
364+ if let Some ( id) = & self . state . get_sandbox_id {
365+ sandbox. metadata . as_mut ( ) . unwrap ( ) . id . clone_from ( id) ;
366+ }
357367 Ok ( Response :: new ( proto:: SandboxResponse {
358- sandbox : Some ( sandbox_with_phase ( & name , phase ) ) ,
368+ sandbox : Some ( sandbox ) ,
359369 } ) )
360370 }
361371
@@ -429,6 +439,9 @@ impl OpenShell for TestOpenShell {
429439 * self . state . last_delete_name . lock ( ) . await = Some ( req. name ) ;
430440 * self . state . last_delete_workspace . lock ( ) . await =
431441 selected_workspace ( & req. workspace_scope ) . map ( str:: to_string) ;
442+ if let Some ( response) = & self . state . delete_response {
443+ return Ok ( Response :: new ( response. clone ( ) ) ) ;
444+ }
432445 Ok ( Response :: new ( proto:: DeleteSandboxResponse {
433446 sandbox_id : String :: new ( ) ,
434447 outcome : proto:: DeletionOutcome :: Completed . into ( ) ,
@@ -1282,19 +1295,154 @@ async fn wait_ready_surfaces_error_phase() {
12821295
12831296#[ tokio:: test]
12841297async fn wait_deleted_returns_when_get_reports_not_found ( ) {
1285- let state = Arc :: new ( MockState {
1286- phase_sequence : vec ! [ proto:: SandboxPhase :: Deleting ] ,
1287- not_found_after : Some ( 2 ) ,
1288- ..Default :: default ( )
1289- } ) ;
1290- let endpoint = start_mock ( state. clone ( ) ) . await ;
1291- let client = connect ( & endpoint) . await ;
1298+ for scoped in [ false , true ] {
1299+ for expected_id in [ None , Some ( "id-my-box" ) ] {
1300+ let state = Arc :: new ( MockState {
1301+ phase_sequence : vec ! [ proto:: SandboxPhase :: Deleting ] ,
1302+ not_found_after : Some ( 1 ) ,
1303+ ..Default :: default ( )
1304+ } ) ;
1305+ let endpoint = start_mock ( state. clone ( ) ) . await ;
1306+ let client = connect ( & endpoint) . await ;
1307+ let timeout = std:: time:: Duration :: from_secs ( 5 ) ;
1308+ if scoped {
1309+ client
1310+ . workspace ( "team" )
1311+ . wait_deleted ( "my-box" , timeout, expected_id)
1312+ . await
1313+ } else {
1314+ client. wait_deleted ( "my-box" , timeout, expected_id) . await
1315+ }
1316+ . unwrap ( ) ;
1317+ assert_eq ! ( state. get_calls. load( Ordering :: SeqCst ) , 2 ) ;
1318+ }
1319+ }
1320+ }
12921321
1293- client
1294- . wait_deleted ( "my-box" , std:: time:: Duration :: from_secs ( 5 ) )
1295- . await
1322+ #[ tokio:: test]
1323+ async fn wait_deleted_completes_on_replacement_after_accepted_deletion ( ) {
1324+ for scoped in [ false , true ] {
1325+ let state = Arc :: new ( MockState {
1326+ get_sandbox_id : Some ( "replacement-id" . to_string ( ) ) ,
1327+ delete_response : Some ( proto:: DeleteSandboxResponse {
1328+ outcome : proto:: DeletionOutcome :: Accepted . into ( ) ,
1329+ sandbox_id : "old-id" . to_string ( ) ,
1330+ } ) ,
1331+ ..Default :: default ( )
1332+ } ) ;
1333+ let endpoint = start_mock ( state. clone ( ) ) . await ;
1334+ let client = connect ( & endpoint) . await ;
1335+ let timeout = std:: time:: Duration :: from_millis ( 50 ) ;
1336+ let result = if scoped {
1337+ client
1338+ . workspace ( "team" )
1339+ . delete_sandbox ( "my-box" , openshell_sdk:: DeleteOptions :: default ( ) )
1340+ . await
1341+ } else {
1342+ client
1343+ . delete_sandbox ( "my-box" , openshell_sdk:: DeleteOptions :: default ( ) )
1344+ . await
1345+ }
12961346 . unwrap ( ) ;
1297- assert ! ( state. get_calls. load( Ordering :: SeqCst ) >= 3 ) ;
1347+ assert_eq ! ( result. outcome, openshell_sdk:: DeletionOutcome :: Accepted ) ;
1348+ assert_eq ! ( result. sandbox_id. as_deref( ) , Some ( "old-id" ) ) ;
1349+ if scoped {
1350+ client
1351+ . workspace ( "team" )
1352+ . wait_deleted ( "my-box" , timeout, result. sandbox_id . as_deref ( ) )
1353+ . await
1354+ } else {
1355+ client
1356+ . wait_deleted ( "my-box" , timeout, result. sandbox_id . as_deref ( ) )
1357+ . await
1358+ }
1359+ . unwrap ( ) ;
1360+ assert_eq ! ( state. get_calls. load( Ordering :: SeqCst ) , 1 ) ;
1361+ let workspace = if scoped { "team" } else { "default" } ;
1362+ assert_eq ! (
1363+ state. last_get_workspace. lock( ) . await . as_deref( ) ,
1364+ Some ( workspace)
1365+ ) ;
1366+ assert_eq ! (
1367+ state. last_delete_workspace. lock( ) . await . as_deref( ) ,
1368+ Some ( workspace)
1369+ ) ;
1370+ }
1371+ }
1372+
1373+ #[ tokio:: test]
1374+ async fn wait_deleted_does_not_complete_while_expected_identity_remains ( ) {
1375+ for scoped in [ false , true ] {
1376+ let state = Arc :: new ( MockState {
1377+ get_sandbox_id : Some ( "old-id" . to_string ( ) ) ,
1378+ ..Default :: default ( )
1379+ } ) ;
1380+ let endpoint = start_mock ( state. clone ( ) ) . await ;
1381+ let client = connect ( & endpoint) . await ;
1382+ let timeout = std:: time:: Duration :: ZERO ;
1383+ let error = if scoped {
1384+ client
1385+ . workspace ( "team" )
1386+ . wait_deleted ( "my-box" , timeout, Some ( "old-id" ) )
1387+ . await
1388+ } else {
1389+ client. wait_deleted ( "my-box" , timeout, Some ( "old-id" ) ) . await
1390+ }
1391+ . unwrap_err ( ) ;
1392+ assert_eq ! ( error. code( ) , "connect" ) ;
1393+ assert ! ( error. to_string( ) . contains( "timed out waiting" ) ) ;
1394+ assert_eq ! ( state. get_calls. load( Ordering :: SeqCst ) , 1 ) ;
1395+ }
1396+ }
1397+
1398+ #[ tokio:: test]
1399+ async fn wait_deleted_without_identity_waits_for_replacement_to_disappear ( ) {
1400+ for scoped in [ false , true ] {
1401+ let state = Arc :: new ( MockState {
1402+ get_sandbox_id : Some ( "replacement-id" . to_string ( ) ) ,
1403+ not_found_after : Some ( 1 ) ,
1404+ ..Default :: default ( )
1405+ } ) ;
1406+ let endpoint = start_mock ( state. clone ( ) ) . await ;
1407+ let client = connect ( & endpoint) . await ;
1408+ let timeout = std:: time:: Duration :: from_secs ( 5 ) ;
1409+ if scoped {
1410+ client
1411+ . workspace ( "team" )
1412+ . wait_deleted ( "my-box" , timeout, None )
1413+ . await
1414+ } else {
1415+ client. wait_deleted ( "my-box" , timeout, None ) . await
1416+ }
1417+ . unwrap ( ) ;
1418+ assert_eq ! ( state. get_calls. load( Ordering :: SeqCst ) , 2 ) ;
1419+ }
1420+ }
1421+
1422+ #[ tokio:: test]
1423+ async fn wait_deleted_propagates_errors_other_than_not_found ( ) {
1424+ for scoped in [ false , true ] {
1425+ for code in [ tonic:: Code :: PermissionDenied , tonic:: Code :: Unavailable ] {
1426+ let state = Arc :: new ( MockState {
1427+ get_error : Some ( Status :: new ( code, "lookup failed" ) ) ,
1428+ ..Default :: default ( )
1429+ } ) ;
1430+ let endpoint = start_mock ( state. clone ( ) ) . await ;
1431+ let client = connect ( & endpoint) . await ;
1432+ let timeout = std:: time:: Duration :: from_secs ( 5 ) ;
1433+ let error = if scoped {
1434+ client
1435+ . workspace ( "team" )
1436+ . wait_deleted ( "my-box" , timeout, Some ( "old-id" ) )
1437+ . await
1438+ } else {
1439+ client. wait_deleted ( "my-box" , timeout, Some ( "old-id" ) ) . await
1440+ }
1441+ . unwrap_err ( ) ;
1442+ assert_eq ! ( error. grpc_status( ) . unwrap( ) . code( ) , code) ;
1443+ assert_eq ! ( state. get_calls. load( Ordering :: SeqCst ) , 1 ) ;
1444+ }
1445+ }
12981446}
12991447
13001448#[ tokio:: test]
0 commit comments