#![allow(dead_code)] use std::env; use std::fmt::{Display, Formatter}; use std::fs; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; use crate::session::{Session, SessionError}; /// Per-worktree session store that namespaces on-disk session files by /// workspace fingerprint so that parallel `opencode serve` instances never /// collide. /// /// Create via [`SessionStore::from_cwd`] (derives the store path from the /// server's working directory) or [`SessionStore::from_data_dir`] (honours an /// explicit `--data-dir` flag). Both constructors produce a directory layout /// of `/sessions//` where `` is a /// stable hex digest of the canonical workspace root. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionStore { /// Resolved root of the session namespace, e.g. /// `/home/user/project/.claw/sessions/a1b2c3d4e5f60718/`. sessions_root: PathBuf, /// The canonical workspace path that was fingerprinted. workspace_root: PathBuf, } impl SessionStore { /// Build a store from the server's current working directory. /// /// The on-disk layout becomes `/.claw/sessions//`. pub fn from_cwd(cwd: impl AsRef) -> Result { let cwd = cwd.as_ref(); let sessions_root = cwd .join(".claw") .join("sessions") .join(workspace_fingerprint(cwd)); fs::create_dir_all(&sessions_root)?; Ok(Self { sessions_root, workspace_root: cwd.to_path_buf(), }) } /// Build a store from an explicit `--data-dir` flag. /// /// The on-disk layout becomes `/sessions//` /// where `` is derived from `workspace_root`. pub fn from_data_dir( data_dir: impl AsRef, workspace_root: impl AsRef, ) -> Result { let workspace_root = workspace_root.as_ref(); let sessions_root = data_dir .as_ref() .join("sessions") .join(workspace_fingerprint(workspace_root)); fs::create_dir_all(&sessions_root)?; Ok(Self { sessions_root, workspace_root: workspace_root.to_path_buf(), }) } /// The fully resolved sessions directory for this namespace. #[must_use] pub fn sessions_dir(&self) -> &Path { &self.sessions_root } /// The workspace root this store is bound to. #[must_use] pub fn workspace_root(&self) -> &Path { &self.workspace_root } #[must_use] pub fn create_handle(&self, session_id: &str) -> SessionHandle { let id = session_id.to_string(); let path = self .sessions_root .join(format!("{id}.{PRIMARY_SESSION_EXTENSION}")); SessionHandle { id, path } } pub fn resolve_reference(&self, reference: &str) -> Result { if is_session_reference_alias(reference) { let latest = self.latest_session()?; return Ok(SessionHandle { id: latest.id, path: latest.path, }); } let direct = PathBuf::from(reference); let candidate = if direct.is_absolute() { direct.clone() } else { self.workspace_root.join(&direct) }; let looks_like_path = direct.extension().is_some() || direct.components().count() > 1; let path = if candidate.exists() { candidate } else if looks_like_path { return Err(SessionControlError::Format( format_missing_session_reference(reference), )); } else { self.resolve_managed_path(reference)? }; Ok(SessionHandle { id: session_id_from_path(&path).unwrap_or_else(|| reference.to_string()), path, }) } pub fn resolve_managed_path(&self, session_id: &str) -> Result { for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] { let path = self.sessions_root.join(format!("{session_id}.{extension}")); if path.exists() { return Ok(path); } } if let Some(legacy_root) = self.legacy_sessions_root() { for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] { let path = legacy_root.join(format!("{session_id}.{extension}")); if !path.exists() { continue; } let session = Session::load_from_path(&path)?; self.validate_loaded_session(&path, &session)?; return Ok(path); } } Err(SessionControlError::Format( format_missing_session_reference(session_id), )) } pub fn list_sessions(&self) -> Result, SessionControlError> { let mut sessions = Vec::new(); self.collect_sessions_from_dir(&self.sessions_root, &mut sessions)?; if let Some(legacy_root) = self.legacy_sessions_root() { self.collect_sessions_from_dir(&legacy_root, &mut sessions)?; } sessions.sort_by(|left, right| { right .modified_epoch_millis .cmp(&left.modified_epoch_millis) .then_with(|| right.id.cmp(&left.id)) }); Ok(sessions) } pub fn latest_session(&self) -> Result { self.list_sessions()? .into_iter() .next() .ok_or_else(|| SessionControlError::Format(format_no_managed_sessions())) } pub fn load_session( &self, reference: &str, ) -> Result { let handle = self.resolve_reference(reference)?; let session = Session::load_from_path(&handle.path)?; self.validate_loaded_session(&handle.path, &session)?; Ok(LoadedManagedSession { handle: SessionHandle { id: session.session_id.clone(), path: handle.path, }, session, }) } pub fn fork_session( &self, session: &Session, branch_name: Option, ) -> Result { let parent_session_id = session.session_id.clone(); let forked = session .fork(branch_name) .with_workspace_root(self.workspace_root.clone()); let handle = self.create_handle(&forked.session_id); let branch_name = forked .fork .as_ref() .and_then(|fork| fork.branch_name.clone()); let forked = forked.with_persistence_path(handle.path.clone()); forked.save_to_path(&handle.path)?; Ok(ForkedManagedSession { parent_session_id, handle, session: forked, branch_name, }) } fn legacy_sessions_root(&self) -> Option { self.sessions_root .parent() .filter(|parent| parent.file_name().is_some_and(|name| name == "sessions")) .map(Path::to_path_buf) } fn validate_loaded_session( &self, session_path: &Path, session: &Session, ) -> Result<(), SessionControlError> { let Some(actual) = session.workspace_root() else { if path_is_within_workspace(session_path, &self.workspace_root) { return Ok(()); } return Err(SessionControlError::Format( format_legacy_session_missing_workspace_root(session_path, &self.workspace_root), )); }; if workspace_roots_match(actual, &self.workspace_root) { return Ok(()); } Err(SessionControlError::WorkspaceMismatch { expected: self.workspace_root.clone(), actual: actual.to_path_buf(), }) } fn collect_sessions_from_dir( &self, directory: &Path, sessions: &mut Vec, ) -> Result<(), SessionControlError> { let entries = match fs::read_dir(directory) { Ok(entries) => entries, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()), Err(err) => return Err(err.into()), }; for entry in entries { let entry = entry?; let path = entry.path(); if !is_managed_session_file(&path) { continue; } let metadata = entry.metadata()?; let modified_epoch_millis = metadata .modified() .ok() .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) .map(|duration| duration.as_millis()) .unwrap_or_default(); let summary = match Session::load_from_path(&path) { Ok(session) => { if self.validate_loaded_session(&path, &session).is_err() { continue; } ManagedSessionSummary { id: session.session_id, path, modified_epoch_millis, message_count: session.messages.len(), parent_session_id: session .fork .as_ref() .map(|fork| fork.parent_session_id.clone()), branch_name: session .fork .as_ref() .and_then(|fork| fork.branch_name.clone()), } } Err(_) => ManagedSessionSummary { id: path .file_stem() .and_then(|value| value.to_str()) .unwrap_or("unknown") .to_string(), path, modified_epoch_millis, message_count: 0, parent_session_id: None, branch_name: None, }, }; sessions.push(summary); } Ok(()) } } /// Stable hex fingerprint of a workspace path. /// /// Uses FNV-1a (64-bit) to produce a 16-char hex string that partitions the /// on-disk session directory per workspace root. #[must_use] pub fn workspace_fingerprint(workspace_root: &Path) -> String { let input = workspace_root.to_string_lossy(); let mut hash = 0xcbf2_9ce4_8422_2325_u64; for byte in input.as_bytes() { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x0100_0000_01b3); } format!("{hash:016x}") } pub const PRIMARY_SESSION_EXTENSION: &str = "jsonl"; pub const LEGACY_SESSION_EXTENSION: &str = "json"; pub const LATEST_SESSION_REFERENCE: &str = "latest"; const SESSION_REFERENCE_ALIASES: &[&str] = &[LATEST_SESSION_REFERENCE, "last", "recent"]; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionHandle { pub id: String, pub path: PathBuf, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManagedSessionSummary { pub id: String, pub path: PathBuf, pub modified_epoch_millis: u128, pub message_count: usize, pub parent_session_id: Option, pub branch_name: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoadedManagedSession { pub handle: SessionHandle, pub session: Session, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForkedManagedSession { pub parent_session_id: String, pub handle: SessionHandle, pub session: Session, pub branch_name: Option, } #[derive(Debug)] pub enum SessionControlError { Io(std::io::Error), Session(SessionError), Format(String), WorkspaceMismatch { expected: PathBuf, actual: PathBuf }, } impl Display for SessionControlError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Io(error) => write!(f, "{error}"), Self::Session(error) => write!(f, "{error}"), Self::Format(error) => write!(f, "{error}"), Self::WorkspaceMismatch { expected, actual } => write!( f, "session workspace mismatch: expected {}, found {}", expected.display(), actual.display() ), } } } impl std::error::Error for SessionControlError {} impl From for SessionControlError { fn from(value: std::io::Error) -> Self { Self::Io(value) } } impl From for SessionControlError { fn from(value: SessionError) -> Self { Self::Session(value) } } pub fn sessions_dir() -> Result { managed_sessions_dir_for(env::current_dir()?) } pub fn managed_sessions_dir_for( base_dir: impl AsRef, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; Ok(store.sessions_dir().to_path_buf()) } pub fn create_managed_session_handle( session_id: &str, ) -> Result { create_managed_session_handle_for(env::current_dir()?, session_id) } pub fn create_managed_session_handle_for( base_dir: impl AsRef, session_id: &str, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; Ok(store.create_handle(session_id)) } pub fn resolve_session_reference(reference: &str) -> Result { resolve_session_reference_for(env::current_dir()?, reference) } pub fn resolve_session_reference_for( base_dir: impl AsRef, reference: &str, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; store.resolve_reference(reference) } pub fn resolve_managed_session_path(session_id: &str) -> Result { resolve_managed_session_path_for(env::current_dir()?, session_id) } pub fn resolve_managed_session_path_for( base_dir: impl AsRef, session_id: &str, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; store.resolve_managed_path(session_id) } #[must_use] pub fn is_managed_session_file(path: &Path) -> bool { path.extension() .and_then(|ext| ext.to_str()) .is_some_and(|extension| { extension == PRIMARY_SESSION_EXTENSION || extension == LEGACY_SESSION_EXTENSION }) } pub fn list_managed_sessions() -> Result, SessionControlError> { list_managed_sessions_for(env::current_dir()?) } pub fn list_managed_sessions_for( base_dir: impl AsRef, ) -> Result, SessionControlError> { let store = SessionStore::from_cwd(base_dir)?; store.list_sessions() } pub fn latest_managed_session() -> Result { latest_managed_session_for(env::current_dir()?) } pub fn latest_managed_session_for( base_dir: impl AsRef, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; store.latest_session() } pub fn load_managed_session(reference: &str) -> Result { load_managed_session_for(env::current_dir()?, reference) } pub fn load_managed_session_for( base_dir: impl AsRef, reference: &str, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; store.load_session(reference) } pub fn fork_managed_session( session: &Session, branch_name: Option, ) -> Result { fork_managed_session_for(env::current_dir()?, session, branch_name) } pub fn fork_managed_session_for( base_dir: impl AsRef, session: &Session, branch_name: Option, ) -> Result { let store = SessionStore::from_cwd(base_dir)?; store.fork_session(session, branch_name) } #[must_use] pub fn is_session_reference_alias(reference: &str) -> bool { SESSION_REFERENCE_ALIASES .iter() .any(|alias| reference.eq_ignore_ascii_case(alias)) } fn session_id_from_path(path: &Path) -> Option { path.file_name() .and_then(|value| value.to_str()) .and_then(|name| { name.strip_suffix(&format!(".{PRIMARY_SESSION_EXTENSION}")) .or_else(|| name.strip_suffix(&format!(".{LEGACY_SESSION_EXTENSION}"))) }) .map(ToOwned::to_owned) } fn format_missing_session_reference(reference: &str) -> String { format!( "session not found: {reference}\nHint: managed sessions live in .claw/sessions/. Try `{LATEST_SESSION_REFERENCE}` for the most recent session or `/session list` in the REPL." ) } fn format_no_managed_sessions() -> String { format!( "no managed sessions found in .claw/sessions/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`." ) } fn format_legacy_session_missing_workspace_root( session_path: &Path, workspace_root: &Path, ) -> String { format!( "legacy session is missing workspace binding: {}\nOpen it from its original workspace or re-save it from {}.", session_path.display(), workspace_root.display() ) } fn workspace_roots_match(left: &Path, right: &Path) -> bool { canonicalize_for_compare(left) == canonicalize_for_compare(right) } fn canonicalize_for_compare(path: &Path) -> PathBuf { fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } fn path_is_within_workspace(path: &Path, workspace_root: &Path) -> bool { canonicalize_for_compare(path).starts_with(canonicalize_for_compare(workspace_root)) } #[cfg(test)] mod tests { use super::{ create_managed_session_handle_for, fork_managed_session_for, is_session_reference_alias, list_managed_sessions_for, load_managed_session_for, resolve_session_reference_for, workspace_fingerprint, ManagedSessionSummary, SessionControlError, SessionStore, LATEST_SESSION_REFERENCE, }; use crate::session::Session; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; fn temp_dir() -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time should be after epoch") .as_nanos(); std::env::temp_dir().join(format!("runtime-session-control-{nanos}")) } fn persist_session(root: &Path, text: &str) -> Session { let mut session = Session::new().with_workspace_root(root.to_path_buf()); session .push_user_text(text) .expect("session message should save"); let handle = create_managed_session_handle_for(root, &session.session_id) .expect("managed session handle should build"); let session = session.with_persistence_path(handle.path.clone()); session .save_to_path(&handle.path) .expect("session should persist"); session } fn wait_for_next_millisecond() { let start = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time should be after epoch") .as_millis(); while SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time should be after epoch") .as_millis() <= start {} } fn summary_by_id<'a>( summaries: &'a [ManagedSessionSummary], id: &str, ) -> &'a ManagedSessionSummary { summaries .iter() .find(|summary| summary.id == id) .expect("session summary should exist") } #[test] fn creates_and_lists_managed_sessions() { // given let root = temp_dir(); fs::create_dir_all(&root).expect("root dir should exist"); let older = persist_session(&root, "older session"); wait_for_next_millisecond(); let newer = persist_session(&root, "newer session"); // when let sessions = list_managed_sessions_for(&root).expect("managed sessions should list"); // then assert_eq!(sessions.len(), 2); assert_eq!(sessions[0].id, newer.session_id); assert_eq!(summary_by_id(&sessions, &older.session_id).message_count, 1); assert_eq!(summary_by_id(&sessions, &newer.session_id).message_count, 1); fs::remove_dir_all(root).expect("temp dir should clean up"); } #[test] fn resolves_latest_alias_and_loads_session_from_workspace_root() { // given let root = temp_dir(); fs::create_dir_all(&root).expect("root dir should exist"); let older = persist_session(&root, "older session"); wait_for_next_millisecond(); let newer = persist_session(&root, "newer session"); // when let handle = resolve_session_reference_for(&root, LATEST_SESSION_REFERENCE) .expect("latest alias should resolve"); let loaded = load_managed_session_for(&root, "recent") .expect("recent alias should load the latest session"); // then assert_eq!(handle.id, newer.session_id); assert_eq!(loaded.handle.id, newer.session_id); assert_eq!(loaded.session.messages.len(), 1); assert_ne!(loaded.handle.id, older.session_id); assert!(is_session_reference_alias("last")); fs::remove_dir_all(root).expect("temp dir should clean up"); } #[test] fn forks_session_into_managed_storage_with_lineage() { // given let root = temp_dir(); fs::create_dir_all(&root).expect("root dir should exist"); let source = persist_session(&root, "parent session"); // when let forked = fork_managed_session_for(&root, &source, Some("incident-review".to_string())) .expect("session should fork"); let sessions = list_managed_sessions_for(&root).expect("managed sessions should list"); let summary = summary_by_id(&sessions, &forked.handle.id); // then assert_eq!(forked.parent_session_id, source.session_id); assert_eq!(forked.branch_name.as_deref(), Some("incident-review")); assert_eq!( summary.parent_session_id.as_deref(), Some(source.session_id.as_str()) ); assert_eq!(summary.branch_name.as_deref(), Some("incident-review")); assert_eq!( forked.session.persistence_path(), Some(forked.handle.path.as_path()) ); fs::remove_dir_all(root).expect("temp dir should clean up"); } // ------------------------------------------------------------------ // Per-worktree session isolation (SessionStore) tests // ------------------------------------------------------------------ fn persist_session_via_store(store: &SessionStore, text: &str) -> Session { let mut session = Session::new().with_workspace_root(store.workspace_root().to_path_buf()); session .push_user_text(text) .expect("session message should save"); let handle = store.create_handle(&session.session_id); let session = session.with_persistence_path(handle.path.clone()); session .save_to_path(&handle.path) .expect("session should persist"); session } #[test] fn workspace_fingerprint_is_deterministic_and_differs_per_path() { // given let path_a = Path::new("/tmp/worktree-alpha"); let path_b = Path::new("/tmp/worktree-beta"); // when let fp_a1 = workspace_fingerprint(path_a); let fp_a2 = workspace_fingerprint(path_a); let fp_b = workspace_fingerprint(path_b); // then assert_eq!(fp_a1, fp_a2, "same path must produce the same fingerprint"); assert_ne!( fp_a1, fp_b, "different paths must produce different fingerprints" ); assert_eq!(fp_a1.len(), 16, "fingerprint must be a 16-char hex string"); } #[test] fn session_store_from_cwd_isolates_sessions_by_workspace() { // given let base = temp_dir(); let workspace_a = base.join("repo-alpha"); let workspace_b = base.join("repo-beta"); fs::create_dir_all(&workspace_a).expect("workspace a should exist"); fs::create_dir_all(&workspace_b).expect("workspace b should exist"); let store_a = SessionStore::from_cwd(&workspace_a).expect("store a should build"); let store_b = SessionStore::from_cwd(&workspace_b).expect("store b should build"); // when let session_a = persist_session_via_store(&store_a, "alpha work"); let _session_b = persist_session_via_store(&store_b, "beta work"); // then — each store only sees its own sessions let list_a = store_a.list_sessions().expect("list a"); let list_b = store_b.list_sessions().expect("list b"); assert_eq!(list_a.len(), 1, "store a should see exactly one session"); assert_eq!(list_b.len(), 1, "store b should see exactly one session"); assert_eq!(list_a[0].id, session_a.session_id); assert_ne!( store_a.sessions_dir(), store_b.sessions_dir(), "session directories must differ across workspaces" ); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_from_data_dir_namespaces_by_workspace() { // given let base = temp_dir(); let data_dir = base.join("global-data"); let workspace_a = PathBuf::from("/tmp/project-one"); let workspace_b = PathBuf::from("/tmp/project-two"); fs::create_dir_all(&data_dir).expect("data dir should exist"); let store_a = SessionStore::from_data_dir(&data_dir, &workspace_a).expect("store a should build"); let store_b = SessionStore::from_data_dir(&data_dir, &workspace_b).expect("store b should build"); // when persist_session_via_store(&store_a, "work in project-one"); persist_session_via_store(&store_b, "work in project-two"); // then assert_ne!( store_a.sessions_dir(), store_b.sessions_dir(), "data-dir stores must namespace by workspace" ); assert_eq!(store_a.list_sessions().expect("list a").len(), 1); assert_eq!(store_b.list_sessions().expect("list b").len(), 1); assert_eq!(store_a.workspace_root(), workspace_a.as_path()); assert_eq!(store_b.workspace_root(), workspace_b.as_path()); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_create_and_load_round_trip() { // given let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let store = SessionStore::from_cwd(&base).expect("store should build"); let session = persist_session_via_store(&store, "round-trip message"); // when let loaded = store .load_session(&session.session_id) .expect("session should load via store"); // then assert_eq!(loaded.handle.id, session.session_id); assert_eq!(loaded.session.messages.len(), 1); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_rejects_legacy_session_from_other_workspace() { // given let base = temp_dir(); let workspace_a = base.join("repo-alpha"); let workspace_b = base.join("repo-beta"); fs::create_dir_all(&workspace_a).expect("workspace a should exist"); fs::create_dir_all(&workspace_b).expect("workspace b should exist"); let store_b = SessionStore::from_cwd(&workspace_b).expect("store b should build"); let legacy_root = workspace_b.join(".claw").join("sessions"); fs::create_dir_all(&legacy_root).expect("legacy root should exist"); let legacy_path = legacy_root.join("legacy-cross.jsonl"); let session = Session::new() .with_workspace_root(workspace_a.clone()) .with_persistence_path(legacy_path.clone()); session .save_to_path(&legacy_path) .expect("legacy session should persist"); // when let err = store_b .load_session("legacy-cross") .expect_err("workspace mismatch should be rejected"); // then match err { SessionControlError::WorkspaceMismatch { expected, actual } => { assert_eq!(expected, workspace_b); assert_eq!(actual, workspace_a); } other => panic!("expected workspace mismatch, got {other:?}"), } fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_loads_safe_legacy_session_from_same_workspace() { // given let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let store = SessionStore::from_cwd(&base).expect("store should build"); let legacy_root = base.join(".claw").join("sessions"); let legacy_path = legacy_root.join("legacy-safe.jsonl"); fs::create_dir_all(&legacy_root).expect("legacy root should exist"); let session = Session::new() .with_workspace_root(base.clone()) .with_persistence_path(legacy_path.clone()); session .save_to_path(&legacy_path) .expect("legacy session should persist"); // when let loaded = store .load_session("legacy-safe") .expect("same-workspace legacy session should load"); // then assert_eq!(loaded.handle.id, session.session_id); assert_eq!(loaded.handle.path, legacy_path); assert_eq!(loaded.session.workspace_root(), Some(base.as_path())); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_loads_unbound_legacy_session_from_same_workspace() { // given let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let store = SessionStore::from_cwd(&base).expect("store should build"); let legacy_root = base.join(".claw").join("sessions"); let legacy_path = legacy_root.join("legacy-unbound.json"); fs::create_dir_all(&legacy_root).expect("legacy root should exist"); let session = Session::new().with_persistence_path(legacy_path.clone()); session .save_to_path(&legacy_path) .expect("legacy session should persist"); // when let loaded = store .load_session("legacy-unbound") .expect("same-workspace legacy session without workspace binding should load"); // then assert_eq!(loaded.handle.path, legacy_path); assert_eq!(loaded.session.workspace_root(), None); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_latest_and_resolve_reference() { // given let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let store = SessionStore::from_cwd(&base).expect("store should build"); let _older = persist_session_via_store(&store, "older"); wait_for_next_millisecond(); let newer = persist_session_via_store(&store, "newer"); // when let latest = store.latest_session().expect("latest should resolve"); let handle = store .resolve_reference("latest") .expect("latest alias should resolve"); // then assert_eq!(latest.id, newer.session_id); assert_eq!(handle.id, newer.session_id); fs::remove_dir_all(base).expect("temp dir should clean up"); } #[test] fn session_store_fork_stays_in_same_namespace() { // given let base = temp_dir(); fs::create_dir_all(&base).expect("base dir should exist"); let store = SessionStore::from_cwd(&base).expect("store should build"); let source = persist_session_via_store(&store, "parent work"); // when let forked = store .fork_session(&source, Some("bugfix".to_string())) .expect("fork should succeed"); let sessions = store.list_sessions().expect("list sessions"); // then assert_eq!( sessions.len(), 2, "forked session must land in the same namespace" ); assert_eq!(forked.parent_session_id, source.session_id); assert_eq!(forked.branch_name.as_deref(), Some("bugfix")); assert!( forked.handle.path.starts_with(store.sessions_dir()), "forked session path must be inside the store namespace" ); fs::remove_dir_all(base).expect("temp dir should clean up"); } }