diff --git a/src-tauri/src/commands/task.rs b/src-tauri/src/commands/task.rs index b49580a..2fffa40 100644 --- a/src-tauri/src/commands/task.rs +++ b/src-tauri/src/commands/task.rs @@ -6,6 +6,30 @@ use crate::models::project::Project; use crate::AppState; use tauri::State; +const TASK_LIST_RESULT_PREVIEW_MAX_BYTES: usize = 20_000; +const TASK_LIST_ERROR_PREVIEW_MAX_BYTES: usize = 8_000; +const TASK_LIST_TRUNCATION_NOTICE: &str = + "\n\n[... contenu tronque pour preserver la fluidite de l'interface ...]"; + +fn truncate_for_task_list(value: Option, max_bytes: usize) -> Option { + let Some(content) = value else { + return None; + }; + + if content.len() <= max_bytes { + return Some(content); + } + + let mut boundary = max_bytes; + while boundary > 0 && !content.is_char_boundary(boundary) { + boundary -= 1; + } + + let mut truncated = content[..boundary].to_string(); + truncated.push_str(TASK_LIST_TRUNCATION_NOTICE); + Some(truncated) +} + #[tauri::command] pub fn create_agent_task( state: State<'_, AppState>, @@ -55,7 +79,14 @@ pub fn list_agent_tasks( .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; - let tasks = AgentTask::list_by_project(&db, &project_id)?; + let tasks = AgentTask::list_by_project(&db, &project_id)? + .into_iter() + .map(|mut task| { + task.result = truncate_for_task_list(task.result, TASK_LIST_RESULT_PREVIEW_MAX_BYTES); + task.error = truncate_for_task_list(task.error, TASK_LIST_ERROR_PREVIEW_MAX_BYTES); + task + }) + .collect(); Ok(tasks) } @@ -122,3 +153,31 @@ pub async fn cancel_agent_task( AgentTask::cancel(&db, &task_id)?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::truncate_for_task_list; + + #[test] + fn test_truncate_for_task_list_keeps_short_content() { + let input = Some("short".to_string()); + let output = truncate_for_task_list(input, 10); + assert_eq!(output.as_deref(), Some("short")); + } + + #[test] + fn test_truncate_for_task_list_truncates_long_content() { + let input = Some("x".repeat(20)); + let output = truncate_for_task_list(input, 10).expect("content should exist"); + assert!(output.starts_with("xxxxxxxxxx")); + assert!(output.contains("contenu tronque")); + } + + #[test] + fn test_truncate_for_task_list_respects_utf8_boundaries() { + let input = Some("éééé".to_string()); + let output = truncate_for_task_list(input, 3).expect("content should exist"); + assert!(output.starts_with("é")); + assert!(!output.starts_with("éé")); + } +} diff --git a/src-tauri/src/models/ticket.rs b/src-tauri/src/models/ticket.rs index d17b980..d2f19ad 100644 --- a/src-tauri/src/models/ticket.rs +++ b/src-tauri/src/models/ticket.rs @@ -53,7 +53,7 @@ const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, project_id, source, source artifact_id, artifact_title, artifact_data, status, analyst_report, developer_report, \ worktree_path, branch_name, detected_at, processed_at FROM processed_tickets"; const SELECT_SUMMARY_COLS: &str = "SELECT id, tracker_id, project_id, source, source_ref, \ - artifact_id, artifact_title, '' AS artifact_data, status, analyst_report, developer_report, \ + artifact_id, artifact_title, '' AS artifact_data, status, NULL AS analyst_report, NULL AS developer_report, \ worktree_path, branch_name, detected_at, processed_at FROM processed_tickets"; impl ProcessedTicket { @@ -823,7 +823,7 @@ mod tests { fn test_list_by_project_summary_omits_artifact_payload() { let (conn, project_id, tracker_id) = setup(); - ProcessedTicket::insert_if_new( + let ticket = ProcessedTicket::insert_if_new( &conn, &project_id, &tracker_id, @@ -831,12 +831,19 @@ mod tests { "Large payload ticket", &"x".repeat(10_000), ) - .expect("insert should succeed"); + .expect("insert should succeed") + .expect("ticket should be inserted"); + ProcessedTicket::set_analyst_report(&conn, &ticket.id, &"A".repeat(10_000)) + .expect("analyst report should be stored"); + ProcessedTicket::set_developer_report(&conn, &ticket.id, &"D".repeat(10_000)) + .expect("developer report should be stored"); let tickets = ProcessedTicket::list_by_project_summary(&conn, &project_id) .expect("summary list should succeed"); assert!(!tickets.is_empty()); assert!(tickets.iter().all(|ticket| ticket.artifact_data.is_empty())); + assert!(tickets.iter().all(|ticket| ticket.analyst_report.is_none())); + assert!(tickets.iter().all(|ticket| ticket.developer_report.is_none())); } }