fix(session): retain recovery when reset snapshot fails [skip ci]

Keep the prior recovery checkpoint if the session store or replacement snapshot cannot be constructed, and surface the localized save failure in the transcript and status. An existing lifecycle case reproduced the lost checkpoint before the fix.

Verified: 18 focused persistence, backlog, and lifecycle checks passed on 2,759 frozen inputs; rustfmt and diff checks passed. Local tests do not establish the original Deepin report or rendered platform acceptance. CW-7553.
This commit is contained in:
CodeWhale Bot committed 2026-10-29 17:11:53 -07:00
1 parent c4a4f42e9c
commit 6a08f03124
2 files changed
+67 -22

No files matched your search

+32 -22
View File
@@ -480,29 +480,39 @@ pub(crate) fn persist_recovery_snapshot(app: &mut App) {
}
pub(crate) fn persist_full_reset_snapshot(app: &mut App) {
if let Ok(manager) = SessionManager::default_location()
&& let Ok(session) = build_session_snapshot(app, &manager)
{
let projection = saved_work_projection(app, &session);
app.current_session_id = Some(session.metadata.id.clone());
if let Err(err) = persist_with_pending_work_boundary(
app,
PersistRequest::CompletedCommit { session },
&projection,
) {
app.status_message = Some(format!(
"To-do list update pending while saving reset snapshot ({err})"
));
let session = SessionManager::default_location()
.map_err(|err| err.to_string())
.and_then(|manager| build_session_snapshot(app, &manager));
let session = match session {
Ok(session) => session,
Err(err) => {
let id = app
.current_session_id
.as_deref()
.map(crate::session_manager::truncate_id)
.unwrap_or("—");
let message = app
.tr(MessageId::SessionSaveFailed)
.replace("{id}", id)
.replace("{error}", &err);
app.add_message(crate::tui::history::HistoryCell::Error {
message: message.clone(),
severity: codewhale_error_taxonomy::ErrorSeverity::Error,
});
app.status_message = Some(message);
return;
}
return;
}
// `/clear` and `/new` are explicit boundaries. Never let an older
// in-flight checkpoint resurrect the session the user just discarded,
// even if the replacement snapshot could not be constructed.
// `build_session_snapshot` reuses `current_session_id`, so this id is the
// discarded session's id when the replacement snapshot could not be built.
if let Some(session_id) = app.current_session_id.clone() {
persistence_actor::persist(PersistRequest::ClearCheckpoint { session_id });
};
let projection = saved_work_projection(app, &session);
app.current_session_id = Some(session.metadata.id.clone());
if let Err(err) = persist_with_pending_work_boundary(
app,
PersistRequest::CompletedCommit { session },
&projection,
) {
app.status_message = Some(format!(
"To-do list update pending while saving reset snapshot ({err})"
));
}
}
+35
View File
@@ -33680,6 +33680,41 @@ async fn offline_queue_lifecycle_preserves_edits_and_explicit_reset() {
assert_eq!(app.current_session_id, active);
assert_eq!(app.input, "preserve on failed resume");
}
let mut reset_app = create_test_app();
let reset_id = "reset-snapshot-build-failure";
reset_app.current_session_id = Some(reset_id.into());
let checkpoint = create_saved_session_with_id_and_mode(
reset_id.into(),
&[text_message("user", "recover this")],
&reset_app.model,
&reset_app.workspace,
0,
None,
None,
);
let checkpoint_path = manager
.save_checkpoint(&checkpoint)
.expect("save checkpoint");
let bad_home = directory.path().join("blocked-state-home");
std::fs::write(&bad_home, b"not a directory").unwrap();
{
let _bad_home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &bad_home);
persist_full_reset_snapshot(&mut reset_app);
}
flush_offline_queue_test_actor(&handle).await;
assert!(
checkpoint_path.exists(),
"failed reset must retain recovery"
);
let failure = reset_app
.status_message
.as_deref()
.expect("failed reset status");
assert!(failure.contains("could not be saved"), "{failure}");
assert!(matches!(
reset_app.history.last(),
Some(HistoryCell::Error { message, .. }) if message == failure
));
assert!(handle.try_send(PersistRequest::Shutdown));
task.await.expect("queue actor shutdown");
}