@@ -50,6 +50,45 @@ async fn insert_message(
5050 . unwrap ( ) ;
5151}
5252
53+ async fn insert_acp_tool_message (
54+ services : & aionui_app:: AppServices ,
55+ conv_id : & str ,
56+ msg_id : & str ,
57+ output : & str ,
58+ created_at : i64 ,
59+ ) {
60+ let repo = aionui_db:: SqliteConversationRepository :: new ( services. database . pool ( ) . clone ( ) ) ;
61+ let msg = aionui_db:: models:: MessageRow {
62+ id : msg_id. into ( ) ,
63+ conversation_id : conv_id. into ( ) ,
64+ msg_id : Some ( msg_id. into ( ) ) ,
65+ r#type : "acp_tool_call" . into ( ) ,
66+ content : serde_json:: json!( {
67+ "session_id" : "session-1" ,
68+ "update" : {
69+ "session_update" : "tool_call" ,
70+ "tool_call_id" : msg_id,
71+ "status" : "completed" ,
72+ "title" : "rg" ,
73+ "kind" : "search" ,
74+ "raw_input" : { "pattern" : "needle" , "path" : "." } ,
75+ "content" : [ {
76+ "type" : "content" ,
77+ "content" : { "type" : "text" , "text" : output }
78+ } ]
79+ }
80+ } )
81+ . to_string ( ) ,
82+ position : Some ( "left" . into ( ) ) ,
83+ status : Some ( "finish" . into ( ) ) ,
84+ hidden : false ,
85+ created_at,
86+ } ;
87+ aionui_db:: IConversationRepository :: insert_message ( & repo, & msg)
88+ . await
89+ . unwrap ( ) ;
90+ }
91+
5392async fn upsert_artifact ( services : & aionui_app:: AppServices , artifact : aionui_db:: ConversationArtifactRow ) {
5493 let repo = aionui_db:: SqliteConversationRepository :: new ( services. database . pool ( ) . clone ( ) ) ;
5594 aionui_db:: IConversationRepository :: upsert_artifact ( & repo, & artifact)
@@ -123,6 +162,132 @@ async fn t8_2_messages_pagination() {
123162 assert_eq ! ( json[ "data" ] [ "has_more" ] , false ) ;
124163}
125164
165+ #[ tokio:: test]
166+ async fn t8_2b_messages_compact_mode_truncates_large_tool_payload ( ) {
167+ let ( mut app, services) = build_app ( ) . await ;
168+ let ( token, csrf) = setup_and_login ( & mut app, & services, "admin" , "StrongP@ss1" ) . await ;
169+ let conv_id = create_conversation ( & mut app, & token, & csrf, "Compact Tool Conv" ) . await ;
170+ let large_output = "match line\n " . repeat ( 10_000 ) ;
171+
172+ insert_acp_tool_message ( & services, & conv_id, "tool-big" , & large_output, 1000 ) . await ;
173+
174+ let resp = app
175+ . clone ( )
176+ . oneshot ( get_with_token (
177+ & format ! ( "/api/conversations/{conv_id}/messages?content_mode=compact" ) ,
178+ & token,
179+ ) )
180+ . await
181+ . unwrap ( ) ;
182+ assert_eq ! ( resp. status( ) , StatusCode :: OK ) ;
183+ let json = body_json ( resp) . await ;
184+ let content = & json[ "data" ] [ "items" ] [ 0 ] [ "content" ] ;
185+ let preview = content[ "update" ] [ "content" ] [ 0 ] [ "content" ] [ "text" ] . as_str ( ) . unwrap ( ) ;
186+
187+ assert_eq ! ( content[ "_compact" ] [ "truncated" ] , true ) ;
188+ assert ! ( preview. len( ) < large_output. len( ) ) ;
189+ assert ! ( !preview. contains( & large_output) ) ;
190+ }
191+
192+ #[ tokio:: test]
193+ async fn t8_2c_get_message_returns_full_tool_payload ( ) {
194+ let ( mut app, services) = build_app ( ) . await ;
195+ let ( token, csrf) = setup_and_login ( & mut app, & services, "admin" , "StrongP@ss1" ) . await ;
196+ let conv_id = create_conversation ( & mut app, & token, & csrf, "Tool Detail Conv" ) . await ;
197+ let large_output = "wide rg output\n " . repeat ( 10_000 ) ;
198+
199+ insert_acp_tool_message ( & services, & conv_id, "tool-detail" , & large_output, 1000 ) . await ;
200+
201+ let resp = app
202+ . clone ( )
203+ . oneshot ( get_with_token (
204+ & format ! ( "/api/conversations/{conv_id}/messages/tool-detail" ) ,
205+ & token,
206+ ) )
207+ . await
208+ . unwrap ( ) ;
209+ assert_eq ! ( resp. status( ) , StatusCode :: OK ) ;
210+ let json = body_json ( resp) . await ;
211+
212+ assert_eq ! (
213+ json[ "data" ] [ "content" ] [ "update" ] [ "content" ] [ 0 ] [ "content" ] [ "text" ]
214+ . as_str( )
215+ . unwrap( ) ,
216+ large_output
217+ ) ;
218+ }
219+
220+ #[ tokio:: test]
221+ async fn t8_2d_get_message_requires_auth ( ) {
222+ let ( mut app, services) = build_app ( ) . await ;
223+ let ( token, csrf) = setup_and_login ( & mut app, & services, "admin" , "StrongP@ss1" ) . await ;
224+ let conv_id = create_conversation ( & mut app, & token, & csrf, "Tool Detail Auth Conv" ) . await ;
225+
226+ let resp = app
227+ . oneshot ( get_request ( & format ! (
228+ "/api/conversations/{conv_id}/messages/tool-detail"
229+ ) ) )
230+ . await
231+ . unwrap ( ) ;
232+
233+ assert_eq ! ( resp. status( ) , StatusCode :: FORBIDDEN ) ;
234+ }
235+
236+ #[ tokio:: test]
237+ async fn t8_2e_get_message_not_found_returns_specific_error ( ) {
238+ let ( mut app, services) = build_app ( ) . await ;
239+ let ( token, csrf) = setup_and_login ( & mut app, & services, "admin" , "StrongP@ss1" ) . await ;
240+ let conv_id = create_conversation ( & mut app, & token, & csrf, "Tool Detail Missing Conv" ) . await ;
241+
242+ let resp = app
243+ . oneshot ( get_with_token (
244+ & format ! ( "/api/conversations/{conv_id}/messages/missing-message" ) ,
245+ & token,
246+ ) )
247+ . await
248+ . unwrap ( ) ;
249+ assert_eq ! ( resp. status( ) , StatusCode :: NOT_FOUND ) ;
250+ let json = body_json ( resp) . await ;
251+
252+ assert_eq ! ( json[ "code" ] , "NOT_FOUND" ) ;
253+ assert ! (
254+ json[ "error" ]
255+ . as_str( )
256+ . unwrap( )
257+ . contains( "Message missing-message not found" )
258+ ) ;
259+ }
260+
261+ #[ tokio:: test]
262+ async fn t8_2f_get_message_does_not_leak_cross_user_conversation ( ) {
263+ let ( mut app, services) = build_app ( ) . await ;
264+ let ( owner_token, owner_csrf) = setup_and_login ( & mut app, & services, "admin" , "StrongP@ss1" ) . await ;
265+ let owner_conv_id = create_conversation ( & mut app, & owner_token, & owner_csrf, "Owner Tool Conv" ) . await ;
266+ insert_acp_tool_message ( & services, & owner_conv_id, "owner-tool" , "private output" , 1000 ) . await ;
267+
268+ let ( other_token, _other_csrf) = setup_and_login ( & mut app, & services, "other-user" , "StrongP@ss2" ) . await ;
269+
270+ let resp = app
271+ . oneshot ( get_with_token (
272+ & format ! ( "/api/conversations/{owner_conv_id}/messages/owner-tool" ) ,
273+ & other_token,
274+ ) )
275+ . await
276+ . unwrap ( ) ;
277+ assert_eq ! ( resp. status( ) , StatusCode :: NOT_FOUND ) ;
278+ let json = body_json ( resp) . await ;
279+
280+ assert_eq ! ( json[ "code" ] , "NOT_FOUND" ) ;
281+ assert ! (
282+ json[ "error" ]
283+ . as_str( )
284+ . unwrap( )
285+ . contains( & format!( "Conversation {owner_conv_id} not found" ) )
286+ ) ;
287+ assert ! ( !json[ "error" ] . as_str( ) . unwrap( ) . contains( "owner-tool" ) ) ;
288+ assert ! ( !json[ "error" ] . as_str( ) . unwrap( ) . contains( "private output" ) ) ;
289+ }
290+
126291#[ tokio:: test]
127292async fn t8_3_messages_order_asc_default ( ) {
128293 let ( mut app, services) = build_app ( ) . await ;
0 commit comments