feat: Add session description editing functionality (#3819)

Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
Anthony Langhorne
2025-08-07 01:59:33 +10:00
committed by GitHub
parent 7e40b930da
commit a5371af4c1
3 changed files with 411 additions and 79 deletions
+123 -2
View File
@@ -7,14 +7,14 @@ use crate::state::AppState;
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
routing::get,
routing::{get, put},
Json, Router,
};
use goose::message::Message;
use goose::session;
use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder};
use goose::session::SessionMetadata;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use utoipa::ToSchema;
@@ -36,6 +36,15 @@ pub struct SessionHistoryResponse {
messages: Vec<Message>,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionMetadataRequest {
/// Updated description (name) for the session (max 200 characters)
description: String,
}
const MAX_DESCRIPTION_LENGTH: usize = 200;
#[derive(Serialize, ToSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SessionInsights {
@@ -300,6 +309,56 @@ async fn get_activity_heatmap(
Ok(Json(result))
}
#[utoipa::path(
put,
path = "/sessions/{session_id}/metadata",
request_body = UpdateSessionMetadataRequest,
params(
("session_id" = String, Path, description = "Unique identifier for the session")
),
responses(
(status = 200, description = "Session metadata updated successfully"),
(status = 400, description = "Bad request - Description too long (max 200 characters)"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Session Management"
)]
// Update session metadata
async fn update_session_metadata(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
Json(request): Json<UpdateSessionMetadataRequest>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
// Validate description length
if request.description.len() > MAX_DESCRIPTION_LENGTH {
return Err(StatusCode::BAD_REQUEST);
}
let session_path = session::get_path(session::Identifier::Name(session_id.clone()))
.map_err(|_| StatusCode::BAD_REQUEST)?;
// Read current metadata
let mut metadata = session::read_metadata(&session_path).map_err(|_| StatusCode::NOT_FOUND)?;
// Update description
metadata.description = request.description;
// Save updated metadata
session::update_metadata(&session_path, &metadata)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::OK)
}
// Configure routes for this module
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
@@ -307,5 +366,67 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/sessions/{session_id}", get(get_session_history))
.route("/sessions/insights", get(get_session_insights))
.route("/sessions/activity-heatmap", get(get_activity_heatmap))
.route(
"/sessions/{session_id}/metadata",
put(update_session_metadata),
)
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_update_session_metadata_request_deserialization() {
// Test that our request struct can be deserialized properly
let json = r#"{"description": "test description"}"#;
let request: UpdateSessionMetadataRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.description, "test description");
}
#[tokio::test]
async fn test_update_session_metadata_request_validation() {
// Test empty description
let empty_request = UpdateSessionMetadataRequest {
description: "".to_string(),
};
assert_eq!(empty_request.description, "");
// Test normal description
let normal_request = UpdateSessionMetadataRequest {
description: "My Session Name".to_string(),
};
assert_eq!(normal_request.description, "My Session Name");
// Test description at max length (should be valid)
let max_length_description = "A".repeat(MAX_DESCRIPTION_LENGTH);
let max_request = UpdateSessionMetadataRequest {
description: max_length_description.clone(),
};
assert_eq!(max_request.description, max_length_description);
assert_eq!(max_request.description.len(), MAX_DESCRIPTION_LENGTH);
// Test description over max length
let over_max_description = "A".repeat(MAX_DESCRIPTION_LENGTH + 1);
let over_max_request = UpdateSessionMetadataRequest {
description: over_max_description.clone(),
};
assert_eq!(over_max_request.description, over_max_description);
assert!(over_max_request.description.len() > MAX_DESCRIPTION_LENGTH);
}
#[tokio::test]
async fn test_description_length_validation() {
// Test the validation logic used in the endpoint
let valid_description = "A".repeat(MAX_DESCRIPTION_LENGTH);
assert!(valid_description.len() <= MAX_DESCRIPTION_LENGTH);
let invalid_description = "A".repeat(MAX_DESCRIPTION_LENGTH + 1);
assert!(invalid_description.len() > MAX_DESCRIPTION_LENGTH);
// Test edge cases
assert!(String::new().len() <= MAX_DESCRIPTION_LENGTH); // Empty string
assert!("Short".len() <= MAX_DESCRIPTION_LENGTH); // Short string
}
}