chore: add tests for session serialization issues (#1985)
This commit is contained in:
@@ -421,4 +421,129 @@ mod tests {
|
||||
// Time part should be 6 digits
|
||||
assert_eq!(parts[1].len(), 6);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_special_characters_and_long_text() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let file_path = dir.path().join("special.jsonl");
|
||||
|
||||
// Insert some problematic JSON-like content between long text
|
||||
let long_text = format!(
|
||||
"Start_of_message\n{}{}SOME_MIDDLE_TEXT{}End_of_message",
|
||||
"A".repeat(100_000),
|
||||
"\"}]\n",
|
||||
"A".repeat(100_000)
|
||||
);
|
||||
|
||||
let special_chars = vec![
|
||||
// Long text
|
||||
long_text.as_str(),
|
||||
// Newlines in different positions
|
||||
"Line 1\nLine 2",
|
||||
"Line 1\r\nLine 2",
|
||||
"\nStart with newline",
|
||||
"End with newline\n",
|
||||
"\n\nMultiple\n\nNewlines\n\n",
|
||||
// JSON special characters
|
||||
"Quote\"in middle",
|
||||
"\"Quote at start",
|
||||
"Quote at end\"",
|
||||
"Multiple\"\"Quotes",
|
||||
"{\"json\": \"looking text\"}",
|
||||
// Unicode and special characters
|
||||
"Unicode: 🦆🤖👾",
|
||||
"Special: \\n \\r \\t",
|
||||
"Mixed: \n\"🦆\"\r\n\\n",
|
||||
// Control characters
|
||||
"Tab\there",
|
||||
"Bell\u{0007}char",
|
||||
"Null\u{0000}char",
|
||||
// Long text with mixed content
|
||||
"A very long message with multiple lines\nand \"quotes\"\nand emojis 🦆\nand \\escaped chars",
|
||||
// Potentially problematic JSON content
|
||||
"}{[]\",\\",
|
||||
"]}}\"\\n\\\"{[",
|
||||
"Edge case: } ] some text",
|
||||
"{\"foo\": \"} ]\"}",
|
||||
"}]",
|
||||
];
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for text in special_chars {
|
||||
messages.push(Message::user().with_text(text));
|
||||
messages.push(Message::assistant().with_text(text));
|
||||
}
|
||||
|
||||
// Write messages with special characters
|
||||
persist_messages(&file_path, &messages, None).await?;
|
||||
|
||||
// Read them back
|
||||
let read_messages = read_messages(&file_path)?;
|
||||
|
||||
// Compare all messages
|
||||
assert_eq!(messages.len(), read_messages.len());
|
||||
for (i, (orig, read)) in messages.iter().zip(read_messages.iter()).enumerate() {
|
||||
assert_eq!(orig.role, read.role, "Role mismatch at message {}", i);
|
||||
assert_eq!(
|
||||
orig.content.len(),
|
||||
read.content.len(),
|
||||
"Content length mismatch at message {}",
|
||||
i
|
||||
);
|
||||
|
||||
if let (Some(MessageContent::Text(orig_text)), Some(MessageContent::Text(read_text))) =
|
||||
(orig.content.first(), read.content.first())
|
||||
{
|
||||
assert_eq!(
|
||||
orig_text.text, read_text.text,
|
||||
"Text mismatch at message {}\nExpected: {}\nGot: {}",
|
||||
i, orig_text.text, read_text.text
|
||||
);
|
||||
} else {
|
||||
panic!("Messages don't match expected structure at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify file format
|
||||
let contents = fs::read_to_string(&file_path)?;
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
// First line should be metadata
|
||||
assert!(
|
||||
lines[0].contains("\"description\""),
|
||||
"First line should be metadata"
|
||||
);
|
||||
|
||||
// Each subsequent line should be valid JSON
|
||||
for (i, line) in lines.iter().enumerate().skip(1) {
|
||||
assert!(
|
||||
serde_json::from_str::<Message>(line).is_ok(),
|
||||
"Invalid JSON at line {}: {}",
|
||||
i + 1,
|
||||
line
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metadata_special_chars() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let file_path = dir.path().join("metadata.jsonl");
|
||||
|
||||
let mut metadata = SessionMetadata::default();
|
||||
metadata.description = "Description with\nnewline and \"quotes\" and 🦆".to_string();
|
||||
|
||||
let messages = vec![Message::user().with_text("test")];
|
||||
|
||||
// Write with special metadata
|
||||
save_messages_with_metadata(&file_path, &metadata, &messages)?;
|
||||
|
||||
// Read back metadata
|
||||
let read_metadata = read_metadata(&file_path)?;
|
||||
assert_eq!(metadata.description, read_metadata.description);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ViewConfig } from '../../App';
|
||||
import { fetchSessionDetails, type SessionDetails } from '../../sessions';
|
||||
import { fetchSharedSessionDetails } from '../../sharedSessions';
|
||||
import SessionListView from './SessionListView';
|
||||
import SessionHistoryView from './SessionHistoryView';
|
||||
import { Card } from '../ui/card';
|
||||
import { Input } from '../ui/input';
|
||||
import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { toastError } from '../../toasts';
|
||||
|
||||
interface SessionsViewProps {
|
||||
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
|
||||
@@ -34,6 +29,12 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
setError('Failed to load session details. Please try again later.');
|
||||
// Keep the selected session null if there's an error
|
||||
setSelectedSession(null);
|
||||
|
||||
toastError({
|
||||
title: 'Failed to load session. The file may be corrupted.',
|
||||
msg: 'Please try again later.',
|
||||
traceback: err,
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingSession(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user