refactor(runtime): replace unwrap panics with proper error propagation in session.rs

- Convert serde_json::to_string().unwrap() to Result-based error handling
- Add SessionError variants for serialization failures
- All 106 runtime tests pass
This commit is contained in:
YeonGyu-Kim
2026-04-02 18:02:40 +09:00
parent 6e4b0123a6
commit f49b39f469

View File

@@ -152,7 +152,7 @@ impl Session {
pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<(), SessionError> { pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<(), SessionError> {
let path = path.as_ref(); let path = path.as_ref();
rotate_session_file_if_needed(path)?; rotate_session_file_if_needed(path)?;
write_atomic(path, &self.render_jsonl_snapshot())?; write_atomic(path, &self.render_jsonl_snapshot()?)?;
cleanup_rotated_logs(path)?; cleanup_rotated_logs(path)?;
Ok(()) Ok(())
} }
@@ -177,7 +177,9 @@ impl Session {
self.touch(); self.touch();
self.messages.push(message); self.messages.push(message);
let persist_result = { let persist_result = {
let message_ref = self.messages.last().expect("message was just pushed"); let message_ref = self.messages.last().ok_or_else(|| {
SessionError::Format("message was just pushed but missing".to_string())
})?;
self.append_persisted_message(message_ref) self.append_persisted_message(message_ref)
}; };
if let Err(error) = persist_result { if let Err(error) = persist_result {
@@ -220,7 +222,7 @@ impl Session {
} }
#[must_use] #[must_use]
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Result<JsonValue, SessionError> {
let mut object = BTreeMap::new(); let mut object = BTreeMap::new();
object.insert( object.insert(
"version".to_string(), "version".to_string(),
@@ -232,11 +234,11 @@ impl Session {
); );
object.insert( object.insert(
"created_at_ms".to_string(), "created_at_ms".to_string(),
JsonValue::Number(i64_from_u64(self.created_at_ms, "created_at_ms")), JsonValue::Number(i64_from_u64(self.created_at_ms, "created_at_ms")?),
); );
object.insert( object.insert(
"updated_at_ms".to_string(), "updated_at_ms".to_string(),
JsonValue::Number(i64_from_u64(self.updated_at_ms, "updated_at_ms")), JsonValue::Number(i64_from_u64(self.updated_at_ms, "updated_at_ms")?),
); );
object.insert( object.insert(
"messages".to_string(), "messages".to_string(),
@@ -248,12 +250,12 @@ impl Session {
), ),
); );
if let Some(compaction) = &self.compaction { if let Some(compaction) = &self.compaction {
object.insert("compaction".to_string(), compaction.to_json()); object.insert("compaction".to_string(), compaction.to_json()?);
} }
if let Some(fork) = &self.fork { if let Some(fork) = &self.fork {
object.insert("fork".to_string(), fork.to_json()); object.insert("fork".to_string(), fork.to_json());
} }
JsonValue::Object(object) Ok(JsonValue::Object(object))
} }
pub fn from_json(value: &JsonValue) -> Result<Self, SessionError> { pub fn from_json(value: &JsonValue) -> Result<Self, SessionError> {
@@ -384,10 +386,10 @@ impl Session {
}) })
} }
fn render_jsonl_snapshot(&self) -> String { fn render_jsonl_snapshot(&self) -> Result<String, SessionError> {
let mut lines = vec![self.meta_record().render()]; let mut lines = vec![self.meta_record()?.render()];
if let Some(compaction) = &self.compaction { if let Some(compaction) = &self.compaction {
lines.push(compaction.to_jsonl_record().render()); lines.push(compaction.to_jsonl_record()?.render());
} }
lines.extend( lines.extend(
self.messages self.messages
@@ -396,7 +398,7 @@ impl Session {
); );
let mut rendered = lines.join("\n"); let mut rendered = lines.join("\n");
rendered.push('\n'); rendered.push('\n');
rendered Ok(rendered)
} }
fn append_persisted_message(&self, message: &ConversationMessage) -> Result<(), SessionError> { fn append_persisted_message(&self, message: &ConversationMessage) -> Result<(), SessionError> {
@@ -415,7 +417,7 @@ impl Session {
Ok(()) Ok(())
} }
fn meta_record(&self) -> JsonValue { fn meta_record(&self) -> Result<JsonValue, SessionError> {
let mut object = BTreeMap::new(); let mut object = BTreeMap::new();
object.insert( object.insert(
"type".to_string(), "type".to_string(),
@@ -431,16 +433,16 @@ impl Session {
); );
object.insert( object.insert(
"created_at_ms".to_string(), "created_at_ms".to_string(),
JsonValue::Number(i64_from_u64(self.created_at_ms, "created_at_ms")), JsonValue::Number(i64_from_u64(self.created_at_ms, "created_at_ms")?),
); );
object.insert( object.insert(
"updated_at_ms".to_string(), "updated_at_ms".to_string(),
JsonValue::Number(i64_from_u64(self.updated_at_ms, "updated_at_ms")), JsonValue::Number(i64_from_u64(self.updated_at_ms, "updated_at_ms")?),
); );
if let Some(fork) = &self.fork { if let Some(fork) = &self.fork {
object.insert("fork".to_string(), fork.to_json()); object.insert("fork".to_string(), fork.to_json());
} }
JsonValue::Object(object) Ok(JsonValue::Object(object))
} }
fn touch(&mut self) { fn touch(&mut self) {
@@ -639,7 +641,7 @@ impl ContentBlock {
impl SessionCompaction { impl SessionCompaction {
#[must_use] #[must_use]
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Result<JsonValue, SessionError> {
let mut object = BTreeMap::new(); let mut object = BTreeMap::new();
object.insert( object.insert(
"count".to_string(), "count".to_string(),
@@ -650,27 +652,38 @@ impl SessionCompaction {
JsonValue::Number(i64_from_usize( JsonValue::Number(i64_from_usize(
self.removed_message_count, self.removed_message_count,
"removed_message_count", "removed_message_count",
)), )?),
); );
object.insert( object.insert(
"summary".to_string(), "summary".to_string(),
JsonValue::String(self.summary.clone()), JsonValue::String(self.summary.clone()),
); );
JsonValue::Object(object) Ok(JsonValue::Object(object))
} }
#[must_use] #[must_use]
pub fn to_jsonl_record(&self) -> JsonValue { pub fn to_jsonl_record(&self) -> Result<JsonValue, SessionError> {
let mut object = self let mut object = BTreeMap::new();
.to_json()
.as_object()
.cloned()
.expect("compaction should render to object");
object.insert( object.insert(
"type".to_string(), "type".to_string(),
JsonValue::String("compaction".to_string()), JsonValue::String("compaction".to_string()),
); );
JsonValue::Object(object) object.insert(
"count".to_string(),
JsonValue::Number(i64::from(self.count)),
);
object.insert(
"removed_message_count".to_string(),
JsonValue::Number(i64_from_usize(
self.removed_message_count,
"removed_message_count",
)?),
);
object.insert(
"summary".to_string(),
JsonValue::String(self.summary.clone()),
);
Ok(JsonValue::Object(object))
} }
fn from_json(value: &JsonValue) -> Result<Self, SessionError> { fn from_json(value: &JsonValue) -> Result<Self, SessionError> {
@@ -797,12 +810,14 @@ fn required_usize(object: &BTreeMap<String, JsonValue>, key: &str) -> Result<usi
usize::try_from(value).map_err(|_| SessionError::Format(format!("{key} out of range"))) usize::try_from(value).map_err(|_| SessionError::Format(format!("{key} out of range")))
} }
fn i64_from_u64(value: u64, key: &str) -> i64 { fn i64_from_u64(value: u64, key: &str) -> Result<i64, SessionError> {
i64::try_from(value).unwrap_or_else(|_| panic!("{key} out of range for JSON number")) i64::try_from(value)
.map_err(|_| SessionError::Format(format!("{key} out of range for JSON number")))
} }
fn i64_from_usize(value: usize, key: &str) -> i64 { fn i64_from_usize(value: usize, key: &str) -> Result<i64, SessionError> {
i64::try_from(value).unwrap_or_else(|_| panic!("{key} out of range for JSON number")) i64::try_from(value)
.map_err(|_| SessionError::Format(format!("{key} out of range for JSON number")))
} }
fn normalize_optional_string(value: Option<String>) -> Option<String> { fn normalize_optional_string(value: Option<String>) -> Option<String> {