#![allow(clippy::unwrap_used)]
use super::*;
use crate::fixture::Fixture;
use crate::fixture::no_rules;
use crate::fixture::rules_for;
use pretty_assertions::assert_eq;
const S: &str = "session-1";
fn log_path(store: &WorkspaceStore, session: &str) -> PathBuf {
store.partition.join("refs").join(format!("{session}.json"))
}
#[test]
fn a_session_whose_log_cannot_be_read_is_refused_not_half_deleted() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
fx.capture("healthy", "turn-2");
let store = fx.store();
let log = log_path(&store, S);
std::fs::write(&log, b"{\"version\":1,\"entr").unwrap();
let outcome = store.delete_sessions(&[S.to_string(), "healthy".to_string()]);
assert!(
outcome.refused.iter().any(|(id, _)| id == S),
"{:?}",
outcome.refused
);
assert!(log.exists(), "the refused session keeps its records");
assert!(
!store.session_exists("healthy"),
"one unreadable session does not block deleting the others"
);
}
#[test]
fn an_unreadable_log_defers_reclamation_without_failing_the_delete() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
fx.capture("healthy", "turn-2");
let store = fx.store();
std::fs::write(log_path(&store, S), b"not json at all").unwrap();
let outcome = store.delete_sessions(&["healthy".to_string()]);
assert!(!store.session_exists("healthy"), "the promise it can keep");
assert_eq!(outcome.reclaimed.manifests_removed, 0);
assert!(
outcome.sweep_error.is_none(),
"deferring is not failing — delete has no preconditions (D9): {:?}",
outcome.sweep_error
);
assert!(outcome.refused.is_empty());
}
#[test]
fn deleting_takes_the_undo_record_with_the_log() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let store = fx.store();
let target = store.target_for_turn("turn-1").unwrap().unwrap();
store
.restore_to(
S,
&target,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
assert!(store.last_restore_target(S).unwrap().is_some());
store.delete_sessions(&[S.to_string()]);
assert!(!store.session_exists(S));
assert_eq!(
store.last_restore_target(S).unwrap(),
None,
"the undo record goes too, or it pins its manifests as a root for good"
);
}
#[test]
fn an_empty_delete_touches_nothing() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let outcome = fx.store().delete_sessions(&[]);
assert_eq!(outcome.reclaimed, GcStats::default());
assert!(outcome.refused.is_empty());
assert!(fx.store().session_exists(S));
}
#[test]
fn a_full_undo_stack_forgets_the_oldest_rewind_not_the_newest() {
let fx = Fixture::new();
fx.write("a.txt", "v0");
let store = fx.store();
fx.capture(S, "turn-0");
let origin = store.target_for_turn("turn-0").unwrap().unwrap();
let passes = crate::refs::MAX_RESTORE_HISTORY + 3;
for i in 1..=passes {
fx.write("a.txt", format!("v{i}"));
store
.restore_to(
S,
&origin,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
}
assert_eq!(fx.read("a.txt"), "v0");
let undo = store.last_restore_target(S).unwrap().unwrap();
store
.restore_to(
S,
&undo,
RestoreKind::Undo { spending: S },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
assert_eq!(
fx.read("a.txt"),
format!("v{passes}"),
"the record on top is the newest rewind's, not one from beyond the cut"
);
}
#[test]
fn a_turn_resolves_to_its_most_complete_capture() {
let fx = Fixture::new();
fx.write("a.txt", "one");
let first = fx.capture(S, "turn-1");
let late = fx.write("late.txt", "pre");
let supplemental = fx
.store()
.attach_pre_edit(
S,
"turn-1",
&late.to_string_lossy(),
&PreEditImage::Existed(b"pre".to_vec()),
)
.unwrap()
.expect("a path outside the scan attaches");
assert_ne!(supplemental, first.id);
assert_eq!(
fx.store()
.target_for_turn("turn-1")
.unwrap()
.unwrap()
.manifest_id(),
supplemental,
"the turn resolves to the extended capture, not the original"
);
}
#[test]
fn an_unknown_turn_resolves_to_nothing() {
let fx = Fixture::new();
assert_eq!(fx.store().target_for_turn("never-happened").unwrap(), None);
}
#[test]
fn attaching_a_path_the_scan_already_covered_adds_nothing() {
let fx = Fixture::new();
let a = fx.write("a.txt", "one");
fx.capture(S, "turn-1");
assert_eq!(
fx.store()
.attach_pre_edit(
S,
"turn-1",
&a.to_string_lossy(),
&PreEditImage::Existed(b"one".to_vec()),
)
.unwrap(),
None
);
}
#[test]
fn a_created_path_is_tombstoned_once() {
let fx = Fixture::new();
let store = fx.store();
let born = fx.path("born.txt");
let key = born.to_string_lossy().into_owned();
let id = store
.attach_pre_edit(S, "turn-1", &key, &PreEditImage::DidNotExist)
.unwrap()
.expect("a created path records that it did not exist");
assert!(store.manifest(&id).unwrap().absent.contains(&key));
assert_eq!(
store
.attach_pre_edit(S, "turn-1", &key, &PreEditImage::DidNotExist)
.unwrap(),
None,
"the second attach says nothing the first did not"
);
}
#[test]
fn tracked_paths_includes_what_was_looked_for_and_not_found() {
let fx = Fixture::new();
fx.write("present.txt", "here");
fx.capture(S, "turn-1");
let gone = fx.path("gone.txt").to_string_lossy().into_owned();
fx.store()
.attach_pre_edit(S, "turn-1", &gone, &PreEditImage::DidNotExist)
.unwrap();
let paths = fx.store().tracked_paths(S).unwrap();
assert!(paths.contains(&fx.path("present.txt").to_string_lossy().into_owned()));
assert!(
paths.contains(&gone),
"a tombstone is an observation, and the safety scope needs it"
);
}
#[test]
fn disk_usage_measures_records_rather_than_content() {
let fx = Fixture::new();
fx.write("big.txt", "x".repeat(200_000));
fx.capture(S, "turn-1");
let records = fx.store().records_disk_usage().unwrap();
assert!(records > 0, "the manifest and log are real files");
assert!(
records < 200_000,
"the 200 kB of content is not charged to the partition: {records}"
);
}
#[test]
fn inheriting_a_log_stops_at_the_named_turn() {
let fx = Fixture::new();
let store = fx.store();
for i in 0..4 {
fx.write("a.txt", format!("v{i}"));
fx.capture(S, &format!("turn-{i}"));
}
assert_eq!(store.inherit_log(S, "fork", "turn-1").unwrap(), 2);
let inherited: Vec<String> = store
.thread_history("fork")
.unwrap()
.into_iter()
.map(|(entry, _)| entry.turn_id)
.collect();
assert_eq!(inherited, vec!["turn-0".to_string(), "turn-1".to_string()]);
}
#[test]
fn a_fork_from_an_unknown_turn_is_empty_but_real() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let store = fx.store();
assert_eq!(store.inherit_log(S, "fork", "never-happened").unwrap(), 0);
assert!(store.session_exists("fork"));
assert!(store.thread_history("fork").unwrap().is_empty());
}
#[test]
fn undo_conflicts_are_empty_when_nothing_moved() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let store = fx.store();
assert!(
store.undo_conflicts(S, &no_rules()).unwrap().is_empty(),
"no rewind, nothing to conflict with"
);
fx.write("a.txt", "two");
let target = store.target_for_turn("turn-1").unwrap().unwrap();
store
.restore_to(
S,
&target,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
assert!(store.undo_conflicts(S, &no_rules()).unwrap().is_empty());
}
#[test]
fn a_change_made_after_a_rewind_is_reported_as_a_conflict() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let store = fx.store();
fx.write("a.txt", "two");
let target = store.target_for_turn("turn-1").unwrap().unwrap();
store
.restore_to(
S,
&target,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
fx.write("a.txt", "three");
assert_eq!(
store.undo_conflicts(S, &no_rules()).unwrap(),
vec![fx.path("a.txt").to_string_lossy().into_owned()]
);
assert!(
store
.undo_conflicts(S, &rules_for(fx.workspace(), "a.txt"))
.unwrap()
.is_empty(),
"a protected path is not a conflict, because an undo would not touch it"
);
}
#[test]
fn a_declared_path_survives_the_process_that_declared_it() {
let fx = Fixture::new();
let outside = fx.path("declared-by-edit.txt");
std::fs::write(&outside, "one").unwrap();
fx.store()
.declare_paths(S, "turn-1", std::slice::from_ref(&outside))
.unwrap();
assert!(fx.store().declared_paths(S).unwrap().contains(&outside));
}
#[test]
fn a_path_past_the_window_is_still_in_the_safety_scope() {
let fx = Fixture::new();
let old = fx.path("edited-long-ago.txt");
let store = fx.store();
store
.declare_paths(S, "turn-0", std::slice::from_ref(&old))
.unwrap();
for i in 1..=crate::declared::DECLARED_WINDOW_TURNS {
store
.declare_paths(S, &format!("turn-{i}"), &[fx.path("recent.txt")])
.unwrap();
}
assert!(
!store.declared_paths(S).unwrap().contains(&old),
"no longer watched"
);
assert!(
store
.tracked_paths(S)
.unwrap()
.contains(&old.to_string_lossy().into_owned()),
"but still observed, so a restore can still act on it"
);
}
#[test]
fn deleting_a_session_drops_its_declared_set() {
let fx = Fixture::new();
fx.write("a.txt", "one");
fx.capture(S, "turn-1");
let store = fx.store();
store
.declare_paths(S, "turn-1", &[fx.path("edited.txt")])
.unwrap();
store.delete_sessions(&[S.to_string()]);
assert_eq!(store.declared_paths(S).unwrap(), Default::default());
}
#[test]
fn a_clean_round_trip_reports_nothing_moved() {
let fx = Fixture::new();
fx.write("kept.txt", "v1");
fx.write("deleted-later.txt", "here");
fx.capture(S, "turn-1");
fx.write("kept.txt", "v2");
fx.remove("deleted-later.txt");
fx.write("added.txt", "new");
let store = fx.store();
let target = store.target_for_turn("turn-1").unwrap().unwrap();
store
.restore_to(
S,
&target,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
assert_eq!(
fx.read("deleted-later.txt"),
"here",
"the rewind put it back"
);
assert_eq!(
store.undo_conflicts(S, &no_rules()).unwrap(),
Vec::<String>::new(),
"nothing moved; the rewind's own work was reported as a conflict"
);
}
#[test]
fn editing_what_the_rewind_recreated_is_a_real_conflict() {
let fx = Fixture::new();
fx.write("a.txt", "original");
fx.capture(S, "turn-1");
fx.remove("a.txt");
let store = fx.store();
let target = store.target_for_turn("turn-1").unwrap().unwrap();
store
.restore_to(
S,
&target,
RestoreKind::Rewind { undo_for: Some(S) },
fx.restore_scope(S),
&no_rules(),
)
.unwrap();
fx.write("a.txt", "someone else's work");
assert_eq!(
store.undo_conflicts(S, &no_rules()).unwrap(),
vec![fx.path("a.txt").to_string_lossy().into_owned()]
);
}