//! Iteration-6 integration tests: `--resume` + persistent //! input history + migration framework scaffold (ADR-0015 §7, //! §9, §12). //! //! Boots no Tokio runtime and no terminal — these tests //! exercise the persistent state behind `--resume` (the //! `last_project` file under the data root) and the input //! history hydration off `history.log`. use std::fs; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rdbms_playground::app::App; use rdbms_playground::cli::{Args, ArgsError}; use rdbms_playground::event::AppEvent; use rdbms_playground::persistence::Persistence; use rdbms_playground::project::{ self, LAST_PROJECT_FILE, Project, read_last_project, write_last_project, }; fn tempdir() -> tempfile::TempDir { tempfile::tempdir().expect("create tempdir") } // --- Args parsing for --resume --------------------------------- #[test] fn args_parses_resume_flag() { let a = Args::parse(["--resume"]).unwrap(); assert!(a.resume); assert!(a.project_path.is_none()); } #[test] fn args_resume_with_positional_path_is_an_error() { let err = Args::parse(["--resume", "/tmp/foo"]).unwrap_err(); assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}"); } #[test] fn args_resume_after_positional_path_also_errors() { let err = Args::parse(["/tmp/foo", "--resume"]).unwrap_err(); assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}"); } #[test] fn args_help_listing_mentions_resume() { assert!(rdbms_playground::cli::help_text().contains("--resume")); } // --- last_project read/write ---------------------------------- #[test] fn last_project_round_trips_through_disk() { let tmp = tempdir(); let target = tmp.path().join("MyProject"); fs::create_dir(&target).unwrap(); write_last_project(tmp.path(), &target).unwrap(); let on_disk = fs::read_to_string(tmp.path().join(LAST_PROJECT_FILE)).unwrap(); assert!(on_disk.contains("MyProject")); assert_eq!(read_last_project(tmp.path()).unwrap(), Some(target)); } #[test] fn last_project_is_overwritten_each_call() { let tmp = tempdir(); let a = tmp.path().join("A"); let b = tmp.path().join("B"); fs::create_dir(&a).unwrap(); fs::create_dir(&b).unwrap(); write_last_project(tmp.path(), &a).unwrap(); write_last_project(tmp.path(), &b).unwrap(); assert_eq!(read_last_project(tmp.path()).unwrap(), Some(b)); } #[test] fn last_project_create_temp_path_resolves_to_existing_dir() { // Sanity: the path we record is in fact something that // exists when --resume tries to reopen it. This protects // against future refactors that might write a placeholder. let tmp = tempdir(); let project = Project::create_temp(tmp.path()).unwrap(); write_last_project(tmp.path(), project.path()).unwrap(); let read_back = read_last_project(tmp.path()).unwrap(); assert_eq!(read_back.as_deref(), Some(project.path())); assert!(read_back.unwrap().exists()); } #[test] fn read_last_project_handles_missing_data_root_directory() { let tmp = tempdir(); let nested = tmp.path().join("does/not/exist/yet"); // Reading from a directory that hasn't been created at // all should be Ok(None), not an error — the runtime's // first launch lands here. assert!(read_last_project(&nested).unwrap().is_none()); } // --- Stale path on resume: read returns Some(path) but the // path does not exist. The runtime is responsible for // surfacing this; we verify the building block here. #[test] fn last_project_returns_stale_path_verbatim_for_runtime_to_detect() { let tmp = tempdir(); let stale = tmp.path().join("Vanished"); write_last_project(tmp.path(), &stale).unwrap(); let read_back = read_last_project(tmp.path()).unwrap(); assert_eq!(read_back.as_deref(), Some(stale.as_path())); assert!(!stale.exists()); } // --- Project lifecycle writes last_project --------------------- // (Smoke test: launching open_or_create then opening again // should be the same as write_last_project + reopen.) // --- History hydration on project open ---------------------- const fn key(code: KeyCode) -> AppEvent { AppEvent::Key(KeyEvent { code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: crossterm::event::KeyEventState::NONE, }) } #[test] fn read_recent_history_returns_empty_when_log_missing() { let tmp = tempdir(); let p = Persistence::new(tmp.path().to_path_buf()); let entries = p.read_recent_history(10).unwrap(); assert!(entries.is_empty()); } #[test] fn read_recent_history_returns_appended_entries_in_order() { let tmp = tempdir(); let project = Project::create_temp(tmp.path()).unwrap(); let p = Persistence::new(project.path().to_path_buf()); p.append_history("create table A with pk").unwrap(); p.append_history("create table B with pk").unwrap(); p.append_history("create table C with pk").unwrap(); let entries = p.read_recent_history(10).unwrap(); assert_eq!( entries, vec![ "create table A with pk".to_string(), "create table B with pk".to_string(), "create table C with pk".to_string(), ] ); } #[test] fn seed_history_replaces_in_memory_history() { let mut app = App::new(); // Pre-existing in-session entries — should be replaced. for c in "abc".chars() { app.update(key(KeyCode::Char(c))); } app.update(key(KeyCode::Enter)); assert_eq!(app.history, vec!["abc".to_string()]); app.seed_history(vec!["x".to_string(), "y".to_string()]); assert_eq!(app.history, vec!["x".to_string(), "y".to_string()]); } #[test] fn seed_history_preserves_chronological_order_for_navigation() { let mut app = App::new(); app.seed_history(vec![ "old".to_string(), "middle".to_string(), "newest".to_string(), ]); // Up should recall "newest" first (the most recent // entry, which is at the back of the vec by convention). app.update(key(KeyCode::Up)); assert_eq!(app.input, "newest"); app.update(key(KeyCode::Up)); assert_eq!(app.input, "middle"); app.update(key(KeyCode::Up)); assert_eq!(app.input, "old"); } #[test] fn project_switched_event_seeds_history_from_payload() { let mut app = App::new(); app.update(AppEvent::ProjectSwitched { display_name: "Foo".to_string(), is_temp: false, history_entries: vec!["aa".to_string(), "bb".to_string()], }); assert_eq!(app.history, vec!["aa".to_string(), "bb".to_string()]); // Up navigates within the seeded entries. app.update(key(KeyCode::Up)); assert_eq!(app.input, "bb"); } #[test] fn data_root_with_no_last_project_is_resume_safe() { let tmp = tempdir(); // Fresh data root with no projects, no last_project. let _project = project::open_or_create(None, Some(tmp.path())).unwrap(); // open_or_create itself doesn't write last_project (the // runtime does, after a successful open). That's fine — // the runtime test would write it. Verify that // read_last_project here returns None as expected. assert!(read_last_project(tmp.path()).unwrap().is_none()); }