Skip to content

Commit a5371af

Browse files
anthonyzanesq
andauthored
feat: Add session description editing functionality (block#3819)
Co-authored-by: Zane Staggs <[email protected]>
1 parent 7e40b93 commit a5371af

File tree

3 files changed

+411
-79
lines changed

3 files changed

+411
-79
lines changed

crates/goose-server/src/routes/session.rs

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ use crate::state::AppState;
77
use axum::{
88
extract::{Path, State},
99
http::{HeaderMap, StatusCode},
10-
routing::get,
10+
routing::{get, put},
1111
Json, Router,
1212
};
1313
use goose::message::Message;
1414
use goose::session;
1515
use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder};
1616
use goose::session::SessionMetadata;
17-
use serde::Serialize;
17+
use serde::{Deserialize, Serialize};
1818
use tracing::{error, info};
1919
use 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")]
4150
pub 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
304363
pub 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

Comments
 (0)