use super::*; #[test] fn walkthrough_uses_packaged_bundle_before_development_and_reports_missing_assets() { let temp = tempfile::tempdir().unwrap(); let packaged = temp .path() .join("application/examples/release1/project.arcadia"); let development = temp.path().join("checkout"); let checkout_entry = development.join("examples/release1/project.arcadia"); let executable = temp.path().join("application/arcadia-studio.exe"); fs::create_dir_all(checkout_entry.parent().unwrap()).unwrap(); fs::write(&checkout_entry, "development source").unwrap(); assert_eq!( resolve_walkthrough(&executable, Some(&development)).unwrap(), checkout_entry ); fs::create_dir_all(packaged.parent().unwrap()).unwrap(); fs::write(&packaged, "packaged source").unwrap(); assert_eq!( resolve_walkthrough(&executable, Some(&development)).unwrap(), packaged ); assert_eq!(resolve_walkthrough(&executable, None).unwrap(), packaged); fs::remove_file(&packaged).unwrap(); let error = resolve_walkthrough(&executable, None).unwrap_err(); assert!(error.contains("complete examples/release1 folder")); assert!( error .replace('\\', "/") .contains(&packaged.display().to_string().replace('\\', "/")), "{error}" ); assert_eq!( fs::read_to_string(checkout_entry).unwrap(), "development source" ); } fn model(directory: &Path, name: &str) -> PathBuf { let path = directory.join(name); fs::write(&path, b"model contents must remain untouched").unwrap(); path } #[test] fn loading_missing_history_does_not_create_files_or_directories() { let temp = tempfile::tempdir().unwrap(); let store = temp.path().join("not-created/recent-models.json"); let state = State::load(store.clone()); assert!(state.recent.is_empty()); assert!(state.notice.is_none()); assert!(!store.parent().unwrap().exists()); } #[test] fn successful_local_history_is_bounded_ordered_deduplicated_and_durable() { let temp = tempfile::tempdir().unwrap(); let store = temp.path().join("settings/recent-models.json"); let mut state = State::load(store.clone()); let files: Vec<_> = (0..15) .map(|i| model(temp.path(), &format!("model-{i}.arcadia"))) .collect(); for (i, path) in files.iter().enumerate() { state.remember(path, &format!("Model {i}")).unwrap(); } assert_eq!(state.recent.len(), MAX_RECENTS); assert_eq!(state.recent[0].entry.name, "Model 14"); assert_eq!(state.recent[11].entry.name, "Model 3"); state.remember(&files[8], "Renamed model").unwrap(); assert_eq!(state.recent.len(), MAX_RECENTS); assert_eq!(state.recent[0].entry.name, "Renamed model"); let loaded = State::load(store); assert!(loaded.notice.is_none()); assert_eq!( loaded.recent.iter().map(|r| &r.entry).collect::>(), state.recent.iter().map(|r| &r.entry).collect::>() ); assert!( loaded .recent .iter() .all(|r| r.availability == Availability::Available) ); assert_eq!( fs::read(&files[8]).unwrap(), b"model contents must remain untouched" ); } #[test] fn forgotten_and_missing_files_never_change_the_model_or_other_entries() { let temp = tempfile::tempdir().unwrap(); let store = temp.path().join("recent.json"); let a = model(temp.path(), "a.arcadia"); let b = model(temp.path(), "b.json"); let mut state = State::load(store.clone()); state.remember(&a, "A").unwrap(); state.remember(&b, "B").unwrap(); fs::remove_file(&b).unwrap(); state.refresh(); assert_eq!(state.recent[0].availability, Availability::Missing); assert_eq!(state.recent.len(), 2); assert_eq!( State::load(store.clone()).recent[0].availability, Availability::Missing ); let old = fs::read(&store).unwrap(); assert!(state.remember(&b, "Missing file").is_err()); assert_eq!(fs::read(&store).unwrap(), old); state.forget(&a.canonicalize().unwrap()).unwrap(); assert!(a.exists()); assert_eq!(state.recent.len(), 1); assert_eq!(State::load(store).recent.len(), 1); } #[test] fn unavailable_location_can_reappear_on_refresh_without_losing_identity() { let temp = tempfile::tempdir().unwrap(); let path = model(temp.path(), "removable.arcadia"); let mut state = State::default(); state.remember(&path, "Portable system").unwrap(); fs::remove_file(&path).unwrap(); fs::create_dir(&path).unwrap(); state.refresh(); assert!(matches!( state.recent[0].availability, Availability::Unavailable(_) )); fs::remove_dir(&path).unwrap(); fs::write(&path, b"restored").unwrap(); state.refresh(); assert_eq!(state.recent[0].availability, Availability::Available); assert_eq!(state.recent[0].entry.name, "Portable system"); } #[test] fn corrupt_future_and_oversized_history_are_preserved_with_usable_session_history() { let temp = tempfile::tempdir().unwrap(); let model = model(temp.path(), "model.arcadia"); let store = temp.path().join("recent.json"); for bytes in [ b"not json".to_vec(), br#"{"version":99,"entries":[]}"#.to_vec(), vec![b' '; MAX_STORE_BYTES as usize + 1], ] { fs::write(&store, &bytes).unwrap(); let mut state = State::load(store.clone()); assert!(state.notice.is_some()); assert!(state.remember(&model, "Session model").is_err()); assert_eq!(state.recent.len(), 1); assert_eq!(fs::read(&store).unwrap(), bytes); } } #[test] fn failed_metadata_replace_keeps_previous_history_and_reports_memory_only_entry() { let temp = tempfile::tempdir().unwrap(); let a = model(temp.path(), "a.arcadia"); let b = model(temp.path(), "b.arcadia"); let parent = temp.path().join("settings"); let store = parent.join("recent.json"); let mut state = State::load(store.clone()); state.remember(&a, "A").unwrap(); let original = fs::read(&store).unwrap(); let preserved = temp.path().join("preserved"); fs::rename(&parent, &preserved).unwrap(); fs::write(&parent, b"directory unavailable").unwrap(); assert!(state.remember(&b, "B").is_err()); assert!(state.notice.is_some()); assert_eq!(state.recent[0].entry.name, "B"); assert_eq!(fs::read(preserved.join("recent.json")).unwrap(), original); fs::remove_file(&parent).unwrap(); fs::rename(&preserved, &parent).unwrap(); state.remember(&b, "B").unwrap(); assert!(state.notice.is_none()); assert_eq!(State::load(store).recent[0].entry.name, "B"); assert_eq!( fs::read_dir(parent).unwrap().count(), 1, "no abandoned temp files" ); } #[test] fn malformed_entries_and_unknown_fields_never_become_open_requests() { let temp = tempfile::tempdir().unwrap(); let store = temp.path().join("recent.json"); for value in [ serde_json::json!({"version":1,"entries":[{"path":"relative.arcadia","name":"Bad"}]}), serde_json::json!({"version":1,"entries":[],"credential":"not accepted"}), serde_json::json!({"version":1,"entries":[{"path":temp.path().join("absolute.arcadia"),"name":"x".repeat(161)}]}), ] { fs::write(&store, serde_json::to_vec(&value).unwrap()).unwrap(); let state = State::load(store.clone()); assert!(state.notice.is_some()); assert!(state.recent.is_empty()); } } #[test] fn first_and_replaced_metadata_are_parseable_and_contain_no_model_bytes() { let temp = tempfile::tempdir().unwrap(); let path = model(temp.path(), "a.arcadia"); let store = temp.path().join("recent.json"); let mut state = State::load(store.clone()); for name in ["First", "Updated"] { state.remember(&path, name).unwrap(); let bytes = fs::read(&store).unwrap(); let parsed: Stored = serde_json::from_slice(&bytes).unwrap(); assert_eq!(parsed.entries[0].name, name); assert!(!String::from_utf8(bytes).unwrap().contains("model contents")); } }