From c424f3ec08c72a6c63966408e3b3b98bd23c8932 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Fri, 1 May 2026 01:51:35 -0500 Subject: [PATCH] test(persistence): cover schema-version rejection in session + runtime_threads (#233) The architecture promises that session_manager, runtime_threads, and task_manager reject persisted state from a newer schema_version on load, so a downgraded binary fails loud instead of silently truncating or corrupting data. Existing tests covered: - session_manager::test_checkpoint_rejects_newer_schema - task_manager (newer task schema rejection) - runtime_threads::store_load_thread_rejects_newer_schema_version Adds the missing coverage for the other persistence paths: - session_manager::test_load_session_rejects_newer_schema - session_manager::test_load_offline_queue_rejects_newer_schema - runtime_threads::store_load_turn_rejects_newer_schema_version - runtime_threads::store_load_item_rejects_newer_schema_version Each writes a JSON file with schema_version = CURRENT + 1 (or 999), loads through the public API, and asserts the error message contains "newer than supported". Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/tui/src/runtime_threads.rs | 48 +++++++++++++++++++++++ crates/tui/src/session_manager.rs | 63 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 69538689..efb3032f 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -2824,6 +2824,54 @@ mod tests { assert_eq!(CURRENT_RUNTIME_SCHEMA_VERSION, 2); } + #[test] + fn store_load_turn_rejects_newer_schema_version() { + let dir = test_runtime_dir(); + let store = RuntimeThreadStore::open(dir.clone()).expect("open store"); + + let mut turn = sample_turn("thr_t", "trn_future", RuntimeTurnStatus::InProgress); + turn.schema_version = CURRENT_RUNTIME_SCHEMA_VERSION + 1; + + let path = store.turns_dir.join(format!("{}.json", turn.id)); + std::fs::create_dir_all(path.parent().unwrap()).expect("mkdirs"); + std::fs::write(&path, serde_json::to_string(&turn).expect("serialize turn")) + .expect("write turn"); + + let err = store + .load_turn(&turn.id) + .expect_err("load_turn must reject newer schema"); + assert!( + format!("{err:#}").contains("newer than supported"), + "got: {err:#}" + ); + + let _ = std::fs::remove_dir_all(dir); + } + + #[test] + fn store_load_item_rejects_newer_schema_version() { + let dir = test_runtime_dir(); + let store = RuntimeThreadStore::open(dir.clone()).expect("open store"); + + let mut item = sample_item("trn_t", "itm_future", TurnItemLifecycleStatus::InProgress); + item.schema_version = CURRENT_RUNTIME_SCHEMA_VERSION + 1; + + let path = store.items_dir.join(format!("{}.json", item.id)); + std::fs::create_dir_all(path.parent().unwrap()).expect("mkdirs"); + std::fs::write(&path, serde_json::to_string(&item).expect("serialize item")) + .expect("write item"); + + let err = store + .load_item(&item.id) + .expect_err("load_item must reject newer schema"); + assert!( + format!("{err:#}").contains("newer than supported"), + "got: {err:#}" + ); + + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn enforce_lru_capacity_does_not_loop_when_all_threads_are_active() { let mut active = ActiveThreads::default(); diff --git a/crates/tui/src/session_manager.rs b/crates/tui/src/session_manager.rs index 9d463b8d..f30794ca 100644 --- a/crates/tui/src/session_manager.rs +++ b/crates/tui/src/session_manager.rs @@ -809,4 +809,67 @@ mod tests { let err = manager.load_checkpoint().expect_err("should reject schema"); assert!(err.to_string().contains("newer than supported")); } + + #[test] + fn test_load_session_rejects_newer_schema() { + let tmp = tempdir().expect("tempdir"); + let sessions_dir = tmp.path().join("sessions"); + let manager = SessionManager::new(sessions_dir.clone()).expect("new"); + + let id = "future-session"; + let path = sessions_dir.join(format!("{id}.json")); + fs::write( + &path, + r#"{ + "schema_version": 999, + "metadata": { + "id": "future-session", + "title": "future", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "message_count": 0, + "total_tokens": 0, + "model": "m", + "workspace": "/tmp", + "mode": null + }, + "messages": [], + "system_prompt": null + }"#, + ) + .expect("write session"); + + let err = manager.load_session(id).expect_err("should reject schema"); + assert!( + err.to_string().contains("newer than supported"), + "unexpected error: {err}" + ); + } + + #[test] + fn test_load_offline_queue_rejects_newer_schema() { + let tmp = tempdir().expect("tempdir"); + let sessions_dir = tmp.path().join("sessions"); + let manager = SessionManager::new(sessions_dir.clone()).expect("new"); + let checkpoints = sessions_dir.join("checkpoints"); + fs::create_dir_all(&checkpoints).expect("create checkpoints dir"); + let path = checkpoints.join("offline_queue.json"); + fs::write( + &path, + r#"{ + "schema_version": 999, + "messages": [], + "draft": null + }"#, + ) + .expect("write queue"); + + let err = manager + .load_offline_queue_state() + .expect_err("should reject schema"); + assert!( + err.to_string().contains("newer than supported"), + "unexpected error: {err}" + ); + } }