@@ -32,7 +32,7 @@ def test_executes_command_and_persists_state(tmp_path: Path) -> None:
3232 updates = middleware .before_agent (state , None )
3333 if updates :
3434 state .update (updates )
35- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
35+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
3636
3737 middleware ._run_shell_tool (resources , {"command" : "cd /" }, tool_call_id = None )
3838 result = middleware ._run_shell_tool (resources , {"command" : "pwd" }, tool_call_id = None )
@@ -55,14 +55,14 @@ def test_restart_resets_session_environment(tmp_path: Path) -> None:
5555 updates = middleware .before_agent (state , None )
5656 if updates :
5757 state .update (updates )
58- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
58+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
5959
6060 middleware ._run_shell_tool (resources , {"command" : "export FOO=bar" }, tool_call_id = None )
6161 restart_message = middleware ._run_shell_tool (
6262 resources , {"restart" : True }, tool_call_id = None
6363 )
6464 assert "restarted" in restart_message .lower ()
65- resources = middleware ._ensure_resources (state ) # reacquire after restart
65+ resources = middleware ._get_or_create_resources (state ) # reacquire after restart
6666 result = middleware ._run_shell_tool (
6767 resources , {"command" : "echo ${FOO:-unset}" }, tool_call_id = None
6868 )
@@ -81,7 +81,7 @@ def test_truncation_indicator_present(tmp_path: Path) -> None:
8181 updates = middleware .before_agent (state , None )
8282 if updates :
8383 state .update (updates )
84- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
84+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
8585 result = middleware ._run_shell_tool (resources , {"command" : "seq 1 20" }, tool_call_id = None )
8686 assert "Output truncated" in result
8787 finally :
@@ -98,7 +98,7 @@ def test_timeout_returns_error(tmp_path: Path) -> None:
9898 updates = middleware .before_agent (state , None )
9999 if updates :
100100 state .update (updates )
101- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
101+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
102102 start = time .monotonic ()
103103 result = middleware ._run_shell_tool (resources , {"command" : "sleep 2" }, tool_call_id = None )
104104 elapsed = time .monotonic () - start
@@ -120,7 +120,7 @@ def test_redaction_policy_applies(tmp_path: Path) -> None:
120120 updates = middleware .before_agent (state , None )
121121 if updates :
122122 state .update (updates )
123- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
123+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
124124 message = middleware ._run_shell_tool (
125125 resources ,
126126 {
"command" :
"printf 'Contact: [email protected] \\ n'" },
@@ -222,7 +222,7 @@ def test_normalize_env_coercion(tmp_path: Path) -> None:
222222 updates = middleware .before_agent (state , None )
223223 if updates :
224224 state .update (updates )
225- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
225+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
226226 result = middleware ._run_shell_tool (
227227 resources , {"command" : "echo $NUM $BOOL" }, tool_call_id = None
228228 )
@@ -242,7 +242,7 @@ def test_shell_tool_missing_command_string(tmp_path: Path) -> None:
242242 updates = middleware .before_agent (state , None )
243243 if updates :
244244 state .update (updates )
245- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
245+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
246246
247247 with pytest .raises (ToolException , match = "expects a 'command' string" ):
248248 middleware ._run_shell_tool (resources , {"command" : None }, tool_call_id = None )
@@ -267,7 +267,7 @@ def test_tool_message_formatting_with_id(tmp_path: Path) -> None:
267267 updates = middleware .before_agent (state , None )
268268 if updates :
269269 state .update (updates )
270- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
270+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
271271
272272 result = middleware ._run_shell_tool (
273273 resources , {"command" : "echo test" }, tool_call_id = "test-id-123"
@@ -292,7 +292,7 @@ def test_nonzero_exit_code_returns_error(tmp_path: Path) -> None:
292292 updates = middleware .before_agent (state , None )
293293 if updates :
294294 state .update (updates )
295- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
295+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
296296
297297 result = middleware ._run_shell_tool (
298298 resources ,
@@ -319,7 +319,7 @@ def test_truncation_by_bytes(tmp_path: Path) -> None:
319319 updates = middleware .before_agent (state , None )
320320 if updates :
321321 state .update (updates )
322- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
322+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
323323
324324 result = middleware ._run_shell_tool (
325325 resources , {"command" : "python3 -c 'print(\" x\" * 100)'" }, tool_call_id = None
@@ -379,15 +379,6 @@ def test_shutdown_command_timeout_logged(tmp_path: Path) -> None:
379379 middleware .after_agent (state , None )
380380
381381
382- def test_ensure_resources_missing_state (tmp_path : Path ) -> None :
383- """Test that _ensure_resources raises when resources are missing."""
384- middleware = ShellToolMiddleware (workspace_root = tmp_path / "workspace" )
385- state : AgentState = _empty_state ()
386-
387- with pytest .raises (ToolException , match = "Shell session resources are unavailable" ):
388- middleware ._ensure_resources (state ) # type: ignore[attr-defined]
389-
390-
391382def test_empty_output_replaced_with_no_output (tmp_path : Path ) -> None :
392383 """Test that empty command output is replaced with '<no output>'."""
393384 middleware = ShellToolMiddleware (workspace_root = tmp_path / "workspace" )
@@ -396,7 +387,7 @@ def test_empty_output_replaced_with_no_output(tmp_path: Path) -> None:
396387 updates = middleware .before_agent (state , None )
397388 if updates :
398389 state .update (updates )
399- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
390+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
400391
401392 result = middleware ._run_shell_tool (
402393 resources ,
@@ -419,7 +410,7 @@ def test_stderr_output_labeling(tmp_path: Path) -> None:
419410 updates = middleware .before_agent (state , None )
420411 if updates :
421412 state .update (updates )
422- resources = middleware ._ensure_resources (state ) # type: ignore[attr-defined]
413+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
423414
424415 result = middleware ._run_shell_tool (
425416 resources , {"command" : "echo error >&2" }, tool_call_id = None
@@ -468,3 +459,98 @@ def test_async_methods_delegate_to_sync(tmp_path: Path) -> None:
468459 asyncio .run (middleware .aafter_agent (state , None ))
469460 finally :
470461 pass
462+
463+
464+ def test_shell_middleware_resumable_after_interrupt (tmp_path : Path ) -> None :
465+ """Test that shell middleware is resumable after an interrupt.
466+
467+ This test simulates a scenario where:
468+ 1. The middleware creates a shell session
469+ 2. A command is executed
470+ 3. The agent is interrupted (state is preserved)
471+ 4. The agent resumes with the same state
472+ 5. The shell session is reused (not recreated)
473+ """
474+ workspace = tmp_path / "workspace"
475+ middleware = ShellToolMiddleware (workspace_root = workspace )
476+
477+ # Simulate first execution (before interrupt)
478+ state : AgentState = _empty_state ()
479+ updates = middleware .before_agent (state , None )
480+ if updates :
481+ state .update (updates )
482+
483+ # Get the resources and verify they exist
484+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
485+ initial_session = resources .session
486+ initial_tempdir = resources .tempdir
487+
488+ # Execute a command to set state
489+ middleware ._run_shell_tool (resources , {"command" : "export TEST_VAR=hello" }, tool_call_id = None )
490+
491+ # Simulate interrupt - state is preserved, but we don't call after_agent
492+ # In a real scenario, the state would be checkpointed here
493+
494+ # Simulate resumption - call before_agent again with same state
495+ # This should reuse existing resources, not create new ones
496+ updates = middleware .before_agent (state , None )
497+ if updates :
498+ state .update (updates )
499+
500+ # Get resources again - should be the same session
501+ resumed_resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
502+
503+ # Verify the session was reused (same object reference)
504+ assert resumed_resources .session is initial_session
505+ assert resumed_resources .tempdir is initial_tempdir
506+
507+ # Verify the session state persisted (environment variable still set)
508+ result = middleware ._run_shell_tool (
509+ resumed_resources , {"command" : "echo ${TEST_VAR:-unset}" }, tool_call_id = None
510+ )
511+ assert "hello" in result
512+ assert "unset" not in result
513+
514+ # Clean up
515+ middleware .after_agent (state , None )
516+
517+
518+ def test_get_or_create_resources_creates_when_missing (tmp_path : Path ) -> None :
519+ """Test that _get_or_create_resources creates resources when they don't exist."""
520+ workspace = tmp_path / "workspace"
521+ middleware = ShellToolMiddleware (workspace_root = workspace )
522+
523+ state : AgentState = _empty_state ()
524+
525+ # State has no resources initially
526+ assert "shell_session_resources" not in state
527+
528+ # Call _get_or_create_resources - should create new resources
529+ resources = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
530+
531+ assert isinstance (resources , _SessionResources )
532+ assert resources .session is not None
533+ assert state .get ("shell_session_resources" ) is resources
534+
535+ # Clean up
536+ resources ._finalizer ()
537+
538+
539+ def test_get_or_create_resources_reuses_existing (tmp_path : Path ) -> None :
540+ """Test that _get_or_create_resources reuses existing resources."""
541+ workspace = tmp_path / "workspace"
542+ middleware = ShellToolMiddleware (workspace_root = workspace )
543+
544+ state : AgentState = _empty_state ()
545+
546+ # Create resources first time
547+ resources1 = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
548+
549+ # Call again - should return the same resources
550+ resources2 = middleware ._get_or_create_resources (state ) # type: ignore[attr-defined]
551+
552+ assert resources1 is resources2
553+ assert resources1 .session is resources2 .session
554+
555+ # Clean up
556+ resources1 ._finalizer ()
0 commit comments