@@ -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.
123129fn 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).
342403async 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