Skip to content

Commit a184ecf

Browse files
committed
fix(feedback): report corrupt history entries
1 parent 7954d02 commit a184ecf

1 file changed

Lines changed: 118 additions & 15 deletions

File tree

src/cortex-cli/src/feedback_cmd.rs

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ struct FeedbackEntry {
119119
session_id: Option<String>,
120120
}
121121

122+
#[derive(Debug, Serialize)]
123+
struct FeedbackHistoryError {
124+
path: String,
125+
error: String,
126+
}
127+
122128
/// Get the feedback directory.
123129
fn get_feedback_dir() -> PathBuf {
124130
dirs::home_dir()
@@ -287,26 +293,23 @@ async fn run_history(args: FeedbackHistoryArgs) -> Result<()> {
287293

288294
if !feedback_dir.exists() {
289295
if args.json {
290-
println!("[]");
296+
println!(
297+
"{}",
298+
serde_json::to_string_pretty(&Vec::<FeedbackEntry>::new())?
299+
);
291300
} else {
292301
println!("No feedback history found.");
293302
}
294303
return Ok(());
295304
}
296305

297-
let mut entries = Vec::new();
306+
let (mut entries, errors) = load_feedback_history(&feedback_dir)?;
298307

299-
// Read feedback files
300-
if let Ok(dir_entries) = std::fs::read_dir(&feedback_dir) {
301-
for entry in dir_entries.flatten() {
302-
let path = entry.path();
303-
if path.extension().is_some_and(|e| e == "json")
304-
&& let Ok(content) = std::fs::read_to_string(&path)
305-
&& let Ok(entry) = serde_json::from_str::<FeedbackEntry>(&content)
306-
{
307-
entries.push(entry);
308-
}
309-
}
308+
for error in &errors {
309+
eprintln!(
310+
"Warning: failed to read feedback entry {}: {}",
311+
error.path, error.error
312+
);
310313
}
311314

312315
// Sort by timestamp (newest first)
@@ -316,9 +319,21 @@ async fn run_history(args: FeedbackHistoryArgs) -> Result<()> {
316319
entries.truncate(args.limit);
317320

318321
if args.json {
319-
println!("{}", serde_json::to_string_pretty(&entries)?);
320-
} else if entries.is_empty() {
322+
if errors.is_empty() {
323+
println!("{}", serde_json::to_string_pretty(&entries)?);
324+
} else {
325+
println!(
326+
"{}",
327+
serde_json::to_string_pretty(&serde_json::json!({
328+
"entries": entries,
329+
"errors": errors,
330+
}))?
331+
);
332+
}
333+
} else if entries.is_empty() && errors.is_empty() {
321334
println!("No feedback history found.");
335+
} else if entries.is_empty() {
336+
println!("No valid feedback history found.");
322337
} else {
323338
println!("Feedback History:");
324339
println!("{}", "-".repeat(60));
@@ -338,6 +353,52 @@ async fn run_history(args: FeedbackHistoryArgs) -> Result<()> {
338353
Ok(())
339354
}
340355

356+
fn load_feedback_history(
357+
feedback_dir: &std::path::Path,
358+
) -> Result<(Vec<FeedbackEntry>, Vec<FeedbackHistoryError>)> {
359+
let mut entries = Vec::new();
360+
let mut errors = Vec::new();
361+
362+
for dir_entry in std::fs::read_dir(feedback_dir)? {
363+
let dir_entry = match dir_entry {
364+
Ok(entry) => entry,
365+
Err(error) => {
366+
errors.push(FeedbackHistoryError {
367+
path: feedback_dir.display().to_string(),
368+
error: error.to_string(),
369+
});
370+
continue;
371+
}
372+
};
373+
374+
let path = dir_entry.path();
375+
if !path.extension().is_some_and(|e| e == "json") {
376+
continue;
377+
}
378+
379+
let content = match std::fs::read_to_string(&path) {
380+
Ok(content) => content,
381+
Err(error) => {
382+
errors.push(FeedbackHistoryError {
383+
path: path.display().to_string(),
384+
error: error.to_string(),
385+
});
386+
continue;
387+
}
388+
};
389+
390+
match serde_json::from_str::<FeedbackEntry>(&content) {
391+
Ok(entry) => entries.push(entry),
392+
Err(error) => errors.push(FeedbackHistoryError {
393+
path: path.display().to_string(),
394+
error: error.to_string(),
395+
}),
396+
}
397+
}
398+
399+
Ok((entries, errors))
400+
}
401+
341402
/// Submit feedback (save locally and optionally upload).
342403
async fn submit_feedback(
343404
category: &str,
@@ -573,4 +634,46 @@ mod tests {
573634
serde_json::from_str(&pretty_json).expect("deserialization should succeed");
574635
assert_eq!(parsed.id, entry.id);
575636
}
637+
638+
#[test]
639+
fn test_load_feedback_history_reports_corrupt_json() {
640+
let temp_dir = tempfile::tempdir().expect("tempdir should be created");
641+
let valid_entry = FeedbackEntry {
642+
id: "valid-entry".to_string(),
643+
timestamp: "2024-09-01T00:00:00Z".to_string(),
644+
category: "general".to_string(),
645+
message: "Valid feedback".to_string(),
646+
session_id: None,
647+
};
648+
649+
std::fs::write(
650+
temp_dir.path().join("valid.json"),
651+
serde_json::to_string(&valid_entry).expect("valid entry should serialize"),
652+
)
653+
.expect("valid feedback should be written");
654+
std::fs::write(temp_dir.path().join("bad.json"), r#"{"id":"x","#)
655+
.expect("corrupt feedback should be written");
656+
657+
let (entries, errors) =
658+
load_feedback_history(temp_dir.path()).expect("history should be read");
659+
660+
assert_eq!(entries.len(), 1);
661+
assert_eq!(entries[0].id, "valid-entry");
662+
assert_eq!(errors.len(), 1);
663+
assert!(errors[0].path.ends_with("bad.json"));
664+
assert!(!errors[0].error.is_empty());
665+
}
666+
667+
#[test]
668+
fn test_load_feedback_history_reports_only_corrupt_json() {
669+
let temp_dir = tempfile::tempdir().expect("tempdir should be created");
670+
std::fs::write(temp_dir.path().join("bad.json"), r#"{"id":"x","#)
671+
.expect("corrupt feedback should be written");
672+
673+
let (entries, errors) =
674+
load_feedback_history(temp_dir.path()).expect("history should be read");
675+
676+
assert!(entries.is_empty());
677+
assert_eq!(errors.len(), 1);
678+
}
576679
}

0 commit comments

Comments
 (0)