@@ -7,14 +7,11 @@ use rexos::security::SecurityConfig;
77
88mod support;
99
10- #[ tokio:: test]
11- async fn replay_fixture_drives_session_and_tool_calls ( ) {
12- let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
13- "fixtures/replay/session_write_file.json"
14- ) ) ;
15- let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
16-
17- let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
10+ fn fixture_agent (
11+ tmp : & tempfile:: TempDir ,
12+ fixture_base_url : String ,
13+ security : SecurityConfig ,
14+ ) -> ( rexos:: agent:: AgentRuntime , RexosPaths , std:: path:: PathBuf ) {
1815 let paths = RexosPaths {
1916 base_dir : tmp. path ( ) . join ( ".loopforge" ) ,
2017 } ;
@@ -28,14 +25,13 @@ async fn replay_fixture_drives_session_and_tool_calls() {
2825 "fixture" . to_string ( ) ,
2926 ProviderConfig {
3027 kind : ProviderKind :: OpenAiCompatible ,
31- base_url : server . base_url . clone ( ) ,
28+ base_url : fixture_base_url ,
3229 api_key_env : String :: new ( ) ,
3330 default_model : "fixture-model" . to_string ( ) ,
3431 aws_bedrock : None ,
3532 } ,
3633 ) ;
3734
38- let security = SecurityConfig :: default ( ) ;
3935 let cfg = RexosConfig {
4036 llm : Default :: default ( ) ,
4137 providers,
@@ -62,6 +58,23 @@ async fn replay_fixture_drives_session_and_tool_calls() {
6258 let agent =
6359 rexos:: agent:: AgentRuntime :: new_with_security_config ( memory, llms, router, security) ;
6460
61+ ( agent, paths, workspace_root)
62+ }
63+
64+ #[ tokio:: test]
65+ async fn replay_fixture_drives_session_and_tool_calls ( ) {
66+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
67+ "fixtures/replay/session_write_file.json"
68+ ) ) ;
69+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
70+
71+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
72+ let ( agent, _paths, workspace_root) = fixture_agent (
73+ & tmp,
74+ server. base_url . clone ( ) ,
75+ rexos:: security:: SecurityConfig :: default ( ) ,
76+ ) ;
77+
6578 let session_id = "s-replay" ;
6679 agent
6780 . set_session_allowed_tools ( session_id, vec ! [ "fs_write" . to_string( ) ] )
@@ -94,3 +107,136 @@ async fn replay_fixture_drives_session_and_tool_calls() {
94107
95108 server. abort ( ) ;
96109}
110+
111+ #[ tokio:: test]
112+ async fn replay_fixture_blocks_tool_not_in_allowed_tools ( ) {
113+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
114+ "fixtures/replay/session_tool_not_allowed.json"
115+ ) ) ;
116+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
117+
118+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
119+ let ( agent, _paths, workspace_root) = fixture_agent (
120+ & tmp,
121+ server. base_url . clone ( ) ,
122+ rexos:: security:: SecurityConfig :: default ( ) ,
123+ ) ;
124+
125+ let session_id = "s-replay-deny" ;
126+ agent
127+ . set_session_allowed_tools ( session_id, vec ! [ "fs_read" . to_string( ) ] )
128+ . unwrap ( ) ;
129+
130+ let err = agent
131+ . run_session (
132+ workspace_root,
133+ session_id,
134+ None ,
135+ "try write" ,
136+ TaskKind :: Coding ,
137+ )
138+ . await
139+ . unwrap_err ( ) ;
140+ let err_text = err. to_string ( ) ;
141+ assert ! (
142+ err_text. contains( "tool not allowed" ) ,
143+ "expected deny error, got: {err_text}"
144+ ) ;
145+
146+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
147+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
148+
149+ server. abort ( ) ;
150+ }
151+
152+ #[ tokio:: test]
153+ async fn replay_fixture_surfaces_tool_failure_errors ( ) {
154+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
155+ "fixtures/replay/session_tool_failed_invalid_path.json"
156+ ) ) ;
157+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
158+
159+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
160+ let ( agent, _paths, workspace_root) = fixture_agent (
161+ & tmp,
162+ server. base_url . clone ( ) ,
163+ rexos:: security:: SecurityConfig :: default ( ) ,
164+ ) ;
165+
166+ let session_id = "s-replay-tool-failed" ;
167+ agent
168+ . set_session_allowed_tools ( session_id, vec ! [ "fs_write" . to_string( ) ] )
169+ . unwrap ( ) ;
170+
171+ let err = agent
172+ . run_session (
173+ workspace_root,
174+ session_id,
175+ None ,
176+ "write outside workspace" ,
177+ TaskKind :: Coding ,
178+ )
179+ . await
180+ . unwrap_err ( ) ;
181+ let err_text = err. to_string ( ) ;
182+ assert ! (
183+ err_text. contains( "parent traversal" ) ,
184+ "expected relative-path validation error, got: {err_text}"
185+ ) ;
186+
187+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
188+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
189+
190+ server. abort ( ) ;
191+ }
192+
193+ #[ tokio:: test]
194+ async fn replay_fixture_executes_mcp_tool_calls ( ) {
195+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
196+ "fixtures/replay/session_mcp_echo.json"
197+ ) ) ;
198+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
199+
200+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
201+ let ( agent, _paths, workspace_root) = fixture_agent (
202+ & tmp,
203+ server. base_url . clone ( ) ,
204+ rexos:: security:: SecurityConfig :: default ( ) ,
205+ ) ;
206+
207+ let mcp_stub = support:: mcp_stub:: write_mcp_stub ( & workspace_root) ;
208+
209+ let session_id = "s-replay-mcp" ;
210+ agent
211+ . set_session_mcp_config ( session_id, support:: mcp_stub:: mcp_config_json ( & mcp_stub) )
212+ . unwrap ( ) ;
213+ agent
214+ . set_session_allowed_tools ( session_id, vec ! [ "mcp_stub__echo" . to_string( ) ] )
215+ . unwrap ( ) ;
216+
217+ let out = agent
218+ . run_session (
219+ workspace_root,
220+ session_id,
221+ None ,
222+ "call mcp" ,
223+ TaskKind :: Coding ,
224+ )
225+ . await
226+ . unwrap ( ) ;
227+ assert_eq ! ( out, "done" ) ;
228+
229+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
230+ assert_eq ! ( requests. len( ) , 2 , "expected two chat completions calls" ) ;
231+ assert_eq ! ( requests[ 1 ] [ "messages" ] [ 2 ] [ "role" ] , "tool" ) ;
232+ assert_eq ! ( requests[ 1 ] [ "messages" ] [ 2 ] [ "name" ] , "mcp_stub__echo" ) ;
233+ assert_eq ! ( requests[ 1 ] [ "messages" ] [ 2 ] [ "tool_call_id" ] , "call_1" ) ;
234+
235+ let tool_content = requests[ 1 ] [ "messages" ] [ 2 ] [ "content" ]
236+ . as_str ( )
237+ . expect ( "tool message content" ) ;
238+ let tool_json: serde_json:: Value = serde_json:: from_str ( tool_content) . expect ( "tool JSON" ) ;
239+ assert_eq ! ( tool_json[ "content" ] [ 0 ] [ "text" ] , "yo" ) ;
240+
241+ server. abort ( ) ;
242+ }
0 commit comments