@@ -7,14 +7,14 @@ use crate::state::AppState;
77use axum:: {
88 extract:: { Path , State } ,
99 http:: { HeaderMap , StatusCode } ,
10- routing:: get,
10+ routing:: { get, put } ,
1111 Json , Router ,
1212} ;
1313use goose:: message:: Message ;
1414use goose:: session;
1515use goose:: session:: info:: { get_valid_sorted_sessions, SessionInfo , SortOrder } ;
1616use goose:: session:: SessionMetadata ;
17- use serde:: Serialize ;
17+ use serde:: { Deserialize , Serialize } ;
1818use tracing:: { error, info} ;
1919use utoipa:: ToSchema ;
2020
@@ -36,6 +36,15 @@ pub struct SessionHistoryResponse {
3636 messages : Vec < Message > ,
3737}
3838
39+ #[ derive( Deserialize , ToSchema ) ]
40+ #[ serde( rename_all = "camelCase" ) ]
41+ pub struct UpdateSessionMetadataRequest {
42+ /// Updated description (name) for the session (max 200 characters)
43+ description : String ,
44+ }
45+
46+ const MAX_DESCRIPTION_LENGTH : usize = 200 ;
47+
3948#[ derive( Serialize , ToSchema , Debug ) ]
4049#[ serde( rename_all = "camelCase" ) ]
4150pub struct SessionInsights {
@@ -300,12 +309,124 @@ async fn get_activity_heatmap(
300309 Ok ( Json ( result) )
301310}
302311
312+ #[ utoipa:: path(
313+ put,
314+ path = "/sessions/{session_id}/metadata" ,
315+ request_body = UpdateSessionMetadataRequest ,
316+ params(
317+ ( "session_id" = String , Path , description = "Unique identifier for the session" )
318+ ) ,
319+ responses(
320+ ( status = 200 , description = "Session metadata updated successfully" ) ,
321+ ( status = 400 , description = "Bad request - Description too long (max 200 characters)" ) ,
322+ ( status = 401 , description = "Unauthorized - Invalid or missing API key" ) ,
323+ ( status = 404 , description = "Session not found" ) ,
324+ ( status = 500 , description = "Internal server error" )
325+ ) ,
326+ security(
327+ ( "api_key" = [ ] )
328+ ) ,
329+ tag = "Session Management"
330+ ) ]
331+ // Update session metadata
332+ async fn update_session_metadata (
333+ State ( state) : State < Arc < AppState > > ,
334+ headers : HeaderMap ,
335+ Path ( session_id) : Path < String > ,
336+ Json ( request) : Json < UpdateSessionMetadataRequest > ,
337+ ) -> Result < StatusCode , StatusCode > {
338+ verify_secret_key ( & headers, & state) ?;
339+
340+ // Validate description length
341+ if request. description . len ( ) > MAX_DESCRIPTION_LENGTH {
342+ return Err ( StatusCode :: BAD_REQUEST ) ;
343+ }
344+
345+ let session_path = session:: get_path ( session:: Identifier :: Name ( session_id. clone ( ) ) )
346+ . map_err ( |_| StatusCode :: BAD_REQUEST ) ?;
347+
348+ // Read current metadata
349+ let mut metadata = session:: read_metadata ( & session_path) . map_err ( |_| StatusCode :: NOT_FOUND ) ?;
350+
351+ // Update description
352+ metadata. description = request. description ;
353+
354+ // Save updated metadata
355+ session:: update_metadata ( & session_path, & metadata)
356+ . await
357+ . map_err ( |_| StatusCode :: INTERNAL_SERVER_ERROR ) ?;
358+
359+ Ok ( StatusCode :: OK )
360+ }
361+
303362// Configure routes for this module
304363pub fn routes ( state : Arc < AppState > ) -> Router {
305364 Router :: new ( )
306365 . route ( "/sessions" , get ( list_sessions) )
307366 . route ( "/sessions/{session_id}" , get ( get_session_history) )
308367 . route ( "/sessions/insights" , get ( get_session_insights) )
309368 . route ( "/sessions/activity-heatmap" , get ( get_activity_heatmap) )
369+ . route (
370+ "/sessions/{session_id}/metadata" ,
371+ put ( update_session_metadata) ,
372+ )
310373 . with_state ( state)
311374}
375+
376+ #[ cfg( test) ]
377+ mod tests {
378+ use super :: * ;
379+
380+ #[ tokio:: test]
381+ async fn test_update_session_metadata_request_deserialization ( ) {
382+ // Test that our request struct can be deserialized properly
383+ let json = r#"{"description": "test description"}"# ;
384+ let request: UpdateSessionMetadataRequest = serde_json:: from_str ( json) . unwrap ( ) ;
385+ assert_eq ! ( request. description, "test description" ) ;
386+ }
387+
388+ #[ tokio:: test]
389+ async fn test_update_session_metadata_request_validation ( ) {
390+ // Test empty description
391+ let empty_request = UpdateSessionMetadataRequest {
392+ description : "" . to_string ( ) ,
393+ } ;
394+ assert_eq ! ( empty_request. description, "" ) ;
395+
396+ // Test normal description
397+ let normal_request = UpdateSessionMetadataRequest {
398+ description : "My Session Name" . to_string ( ) ,
399+ } ;
400+ assert_eq ! ( normal_request. description, "My Session Name" ) ;
401+
402+ // Test description at max length (should be valid)
403+ let max_length_description = "A" . repeat ( MAX_DESCRIPTION_LENGTH ) ;
404+ let max_request = UpdateSessionMetadataRequest {
405+ description : max_length_description. clone ( ) ,
406+ } ;
407+ assert_eq ! ( max_request. description, max_length_description) ;
408+ assert_eq ! ( max_request. description. len( ) , MAX_DESCRIPTION_LENGTH ) ;
409+
410+ // Test description over max length
411+ let over_max_description = "A" . repeat ( MAX_DESCRIPTION_LENGTH + 1 ) ;
412+ let over_max_request = UpdateSessionMetadataRequest {
413+ description : over_max_description. clone ( ) ,
414+ } ;
415+ assert_eq ! ( over_max_request. description, over_max_description) ;
416+ assert ! ( over_max_request. description. len( ) > MAX_DESCRIPTION_LENGTH ) ;
417+ }
418+
419+ #[ tokio:: test]
420+ async fn test_description_length_validation ( ) {
421+ // Test the validation logic used in the endpoint
422+ let valid_description = "A" . repeat ( MAX_DESCRIPTION_LENGTH ) ;
423+ assert ! ( valid_description. len( ) <= MAX_DESCRIPTION_LENGTH ) ;
424+
425+ let invalid_description = "A" . repeat ( MAX_DESCRIPTION_LENGTH + 1 ) ;
426+ assert ! ( invalid_description. len( ) > MAX_DESCRIPTION_LENGTH ) ;
427+
428+ // Test edge cases
429+ assert ! ( String :: new( ) . len( ) <= MAX_DESCRIPTION_LENGTH ) ; // Empty string
430+ assert ! ( "Short" . len( ) <= MAX_DESCRIPTION_LENGTH ) ; // Short string
431+ }
432+ }
0 commit comments