From 0fe05b682a78903b348a5bbd329f96537344eab4 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 3 May 2026 07:16:11 -0500 Subject: [PATCH] test(session): pin offline-queue session_id stamping (#487 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #487 fix relies on `save_offline_queue_state` correctly stamping the session id so the load path's mismatch check has something to compare against. The existing `test_offline_queue_round_trip_and_clear` covers serialization + clear but doesn't pin the session_id stamping behavior. Adds `test_offline_queue_stamps_session_id_on_save` which exercises three cases: * `save(state, Some("session-A"))` → loaded session_id is `Some("session-A")`. The stamp made it to disk. * `save(state, Some("session-B"))` → re-saving replaces the stamp; loaded session_id is `Some("session-B")`. No stale ID lingers. * `save(state, None)` → loaded session_id is `None`. The UI's load path treats this as legacy-unscoped and refuses to restore (fail-closed), which is what protects users from pre-#487 queues leaking into new chats. Pure additive coverage. The 2 existing offline-queue tests pass unchanged. --- crates/tui/src/session_manager.rs | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/crates/tui/src/session_manager.rs b/crates/tui/src/session_manager.rs index e7de9bff..ae68339d 100644 --- a/crates/tui/src/session_manager.rs +++ b/crates/tui/src/session_manager.rs @@ -967,6 +967,60 @@ mod tests { ); } + #[test] + fn test_offline_queue_stamps_session_id_on_save() { + // #487: save_offline_queue_state must stamp the supplied + // session id so the load path's mismatch check has something + // to compare against. A queue persisted without a session id + // is the legacy unscoped form which the load path treats as + // stale-risky and refuses to restore. + let tmp = tempdir().expect("tempdir"); + let manager = SessionManager::new(tmp.path().join("sessions")).expect("new"); + + let state = OfflineQueueState { + messages: vec![QueuedSessionMessage { + display: "first parked".to_string(), + skill_instruction: None, + }], + ..OfflineQueueState::default() + }; + + manager + .save_offline_queue_state(&state, Some("session-A")) + .expect("save with session id"); + let loaded = manager + .load_offline_queue_state() + .expect("ok") + .expect("present"); + assert_eq!(loaded.session_id.as_deref(), Some("session-A")); + + // Re-saving with a different session id replaces the stamp. + manager + .save_offline_queue_state(&state, Some("session-B")) + .expect("re-save"); + let reloaded = manager + .load_offline_queue_state() + .expect("ok") + .expect("present"); + assert_eq!(reloaded.session_id.as_deref(), Some("session-B")); + + // Saving without a session id explicitly (None) clears the + // stamp — UI's load path treats that as legacy-unscoped and + // fails closed. + manager + .save_offline_queue_state(&state, None) + .expect("save without session id"); + let unscoped = manager + .load_offline_queue_state() + .expect("ok") + .expect("present"); + assert!( + unscoped.session_id.is_none(), + "save with None must persist a missing session_id, got {:?}", + unscoped.session_id + ); + } + #[test] fn test_session_context_references_round_trip() { let tmp = tempdir().expect("tempdir");