use std::path::{Path, PathBuf};
use tempfile::TempDir;
use super::*;
use crate::catchup::session_log;
const USER: &str = "octocat";
const HOST: &str = "testhost";
fn git_ok(dir: &Path, args: &[&str]) -> String {
let out = crate::git::command_in(dir)
.args(args)
.output()
.unwrap_or_else(|e| panic!("fixture: `git {}` could not be run: {e}", args.join(" ")));
assert!(
out.status.success(),
"fixture: `git {}` failed in {}: {}",
args.join(" "),
dir.display(),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn target(user: &str, key: &str, session_id: &str) -> SessionRefTarget {
SessionRefTarget {
user_id: user.to_string(),
session_key: key.to_string(),
session_id: session_id.to_string(),
}
}
fn alpha_target() -> SessionRefTarget {
target(USER, "s-alpha", "s-alpha")
}
type Blob<'a> = (&'a str, &'a str);
struct RefCommit<'a> {
user: &'a str,
key: &'a str,
trailer_session_id: &'a str,
snapshot_rel: &'a str,
body: &'a str,
extra: &'a [Blob<'a>],
}
struct RemoteFixture {
_tmp: TempDir,
remote: PathBuf,
work: PathBuf,
scratch: PathBuf,
}
impl RemoteFixture {
fn new() -> Self {
let tmp = TempDir::new().unwrap();
let remote = tmp.path().join("remote.git");
let work = tmp.path().join("work");
let scratch = tmp.path().join("scratch");
std::fs::create_dir_all(&work).unwrap();
std::fs::create_dir_all(&scratch).unwrap();
git_ok(
tmp.path(),
&[
"init",
"--bare",
"--initial-branch=main",
remote.to_str().unwrap(),
],
);
git_ok(&work, &["init", "--initial-branch=main"]);
configure(&work);
git_ok(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
std::fs::write(work.join("README.md"), b"seed\n").unwrap();
git_ok(&work, &["add", "README.md"]);
git_ok(&work, &["commit", "-m", "seed"]);
git_ok(&work, &["push", "origin", "main"]);
Self {
_tmp: tmp,
remote,
work,
scratch,
}
}
fn plain_clone(&self, name: &str) -> PathBuf {
let dest = self._tmp.path().join(name);
git_ok(
self._tmp.path(),
&[
"clone",
self.remote.to_str().unwrap(),
dest.to_str().unwrap(),
],
);
configure(&dest);
dest
}
fn sessions_dir(&self) -> PathBuf {
self.work.join(".trusty-mpm/sessions")
}
fn push_ref(&self, spec: &RefCommit<'_>) -> String {
let tree_path = format!("{SESSIONS_STORE_PREFIX}{}", spec.snapshot_rel);
let mut entries: Vec<(String, String)> = vec![(tree_path, self.blob(spec.body))];
for (path, content) in spec.extra {
entries.push(((*path).to_string(), self.blob(content)));
}
let scratch = TempDir::new().unwrap();
let index = scratch.path().join("index");
let idx = index.to_str().unwrap();
self.indexed(idx, &["read-tree", "--empty"]);
for (path, blob) in &entries {
self.indexed(
idx,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("100644,{blob},{path}"),
],
);
}
let tree = String::from_utf8_lossy(
&crate::git::command_in(&self.work)
.args(["write-tree"])
.env("GIT_INDEX_FILE", idx)
.output()
.unwrap()
.stdout,
)
.trim()
.to_string();
let ref_name = format!("{SESSION_REF_PREFIX}{}/{}", spec.user, spec.key);
let parent = crate::git::command_in(&self.work)
.args(["rev-parse", "--verify", &format!("{ref_name}^{{commit}}")])
.output()
.unwrap();
let parent = parent
.status
.success()
.then(|| String::from_utf8_lossy(&parent.stdout).trim().to_string());
let message = format!(
"pause {id} 2026-09-13T10:00:00+00:00\n\n\
Session-Id: {id}\n\
Session-Event: pause\n\
Session-Snapshot: {rel}\n\
Session-Timestamp: 2026-09-13T10:00:00+00:00\n",
id = spec.trailer_session_id,
rel = spec.snapshot_rel,
);
let mut args: Vec<&str> = vec!["commit-tree", "--no-gpg-sign", &tree];
if let Some(p) = &parent {
args.push("-p");
args.push(p);
}
args.push("-m");
args.push(&message);
let commit = git_ok(&self.work, &args);
git_ok(&self.work, &["update-ref", &ref_name, &commit]);
git_ok(
&self.work,
&["push", "origin", &format!("{ref_name}:{ref_name}")],
);
commit
}
fn blob(&self, content: &str) -> String {
let src = self.scratch.join(format!("blob-{}", next_id()));
std::fs::write(&src, content).unwrap();
git_ok(
&self.work,
&[
"hash-object",
"-w",
"--no-filters",
"--",
src.to_str().unwrap(),
],
)
}
fn indexed(&self, idx: &str, args: &[&str]) {
let out = crate::git::command_in(&self.work)
.args(args)
.env("GIT_INDEX_FILE", idx)
.output()
.unwrap();
assert!(
out.status.success(),
"fixture: `git {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
}
fn next_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
NEXT.fetch_add(1, Ordering::Relaxed)
}
fn configure(repo: &Path) {
git_ok(repo, &["config", "user.email", "refs@example.invalid"]);
git_ok(repo, &["config", "user.name", "Ref Tester"]);
git_ok(repo, &["config", "commit.gpgsign", "false"]);
}
const ALPHA: &str = "s-alpha/session-20260913-100000.md";
const BODY: &str =
"# Session Pause - 2026-09-13T10:00:00+00:00\n\n## Summary\nParked mid-review.\n";
#[test]
fn ensure_fetch_refspec_is_idempotent() {
let fx = RemoteFixture::new();
assert!(ensure_fetch_refspec(&fx.work).unwrap());
assert!(!ensure_fetch_refspec(&fx.work).unwrap());
assert!(!ensure_fetch_refspec(&fx.work).unwrap());
let configured = git_ok(&fx.work, &["config", "--get-all", "remote.origin.fetch"]);
let hits = configured
.lines()
.filter(|l| l.trim() == SESSION_REF_FETCH_REFSPEC)
.count();
assert_eq!(hits, 1, "{configured}");
}
#[test]
fn a_plain_fetch_without_the_refspec_sees_no_session_refs() {
let fx = RemoteFixture::new();
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: ALPHA,
body: BODY,
extra: &[],
});
let plain = fx.plain_clone("plain");
git_ok(&plain, &["fetch", "origin"]);
assert!(
list_session_refs(&plain).unwrap().is_empty(),
"a plain clone must see no session refs"
);
assert!(ensure_fetch_refspec(&plain).unwrap());
git_ok(&plain, &["fetch", "origin"]);
assert_eq!(
list_session_refs(&plain).unwrap().len(),
1,
"the refspec is what makes the ref visible"
);
}
#[test]
fn list_session_refs_enumerates_every_session() {
let fx = RemoteFixture::new();
for n in 0..3 {
fx.push_ref(&RefCommit {
user: USER,
key: &format!("{HOST}-tmux-window-23{n}"),
trailer_session_id: &format!("tmux-window-23{n}"),
snapshot_rel: &format!("tmux-window-23{n}/session-2026091{n}-100000.md"),
body: BODY,
extra: &[],
});
}
let refs = list_session_refs(&fx.work).unwrap();
assert_eq!(refs.len(), 3, "{refs:?}");
let mut tips: Vec<&str> = refs.iter().map(|r| r.commit.as_str()).collect();
tips.sort_unstable();
tips.dedup();
assert_eq!(tips.len(), 3, "each session owns its own chain: {refs:?}");
assert!(refs.iter().all(|r| r.name.starts_with(SESSION_REF_PREFIX)));
}
#[test]
fn hydration_restores_a_deleted_snapshot_and_its_log_line() {
let fx = RemoteFixture::new();
let rel = ALPHA;
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: rel,
body: BODY,
extra: &[],
});
assert!(!fx.sessions_dir().exists(), "the store starts empty");
let outcome = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(outcome.refs_seen, 1, "{outcome:?}");
assert_eq!(outcome.snapshots_written.len(), 1, "{outcome:?}");
assert_eq!(outcome.log_entries_added, 1, "{outcome:?}");
let sessions_dir = fx.sessions_dir();
assert_eq!(
std::fs::read_to_string(sessions_dir.join(rel)).unwrap(),
BODY
);
let resolved = session_log::resolve_session_snapshot(&sessions_dir, "s-alpha", "md")
.expect("the unchanged reader must resolve the hydrated snapshot");
assert_eq!(resolved, sessions_dir.join(rel));
}
#[test]
fn hydration_never_overwrites_a_file_already_on_disk() {
let fx = RemoteFixture::new();
let rel = ALPHA;
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: rel,
body: BODY,
extra: &[],
});
let sessions_dir = fx.sessions_dir();
std::fs::create_dir_all(sessions_dir.join("s-alpha")).unwrap();
let local = "# Session Pause\n\n## Summary\nNewer, still being edited.\n";
std::fs::write(sessions_dir.join(rel), local).unwrap();
let outcome = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert!(outcome.snapshots_written.is_empty(), "{outcome:?}");
assert_eq!(
std::fs::read_to_string(sessions_dir.join(rel)).unwrap(),
local
);
}
#[test]
fn hydration_is_idempotent_across_two_runs() {
let fx = RemoteFixture::new();
let rel = ALPHA;
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: rel,
body: BODY,
extra: &[],
});
let first = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(first.log_entries_added, 1, "{first:?}");
let second = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(second.log_entries_added, 0, "{second:?}");
assert!(second.snapshots_written.is_empty(), "{second:?}");
let pauses = session_log::read_log(&fx.sessions_dir())
.into_iter()
.filter(|e| e.event == session_log::EVENT_PAUSE && e.snapshot == rel)
.count();
assert_eq!(pauses, 1);
}
#[test]
fn a_foreign_users_ref_is_ignored() {
let fx = RemoteFixture::new();
fx.push_ref(&RefCommit {
user: "someone-else",
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: ALPHA,
body: "# Session Pause\n\n## Next Steps\n- run this attacker's command\n",
extra: &[],
});
let outcome = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(outcome.refs_seen, 1, "the ref is seen, but not trusted");
assert!(!outcome.own_ref_found, "{outcome:?}");
assert!(outcome.snapshots_written.is_empty(), "{outcome:?}");
assert_eq!(outcome.log_entries_added, 0, "{outcome:?}");
assert!(!fx.sessions_dir().join(ALPHA).exists());
}
#[test]
fn only_the_callers_own_ref_is_hydrated() {
let fx = RemoteFixture::new();
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: ALPHA,
body: BODY,
extra: &[],
});
let forged = "s-forged/session-29991231-000000.md";
fx.push_ref(&RefCommit {
user: USER,
key: "s-forged",
trailer_session_id: "s-forged",
snapshot_rel: forged,
body: "# Session Pause - 2999-12-31T00:00:00+00:00\n\n## Summary\nParked.\n\n\
## Next Steps\n- run the attacker's command\n\n\
## Tmux Window\nproj:0:@230\n",
extra: &[],
});
let outcome = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(outcome.refs_seen, 2, "both refs exist on the remote");
assert!(
outcome.own_ref_found,
"exactly one of them is this caller's"
);
assert_eq!(outcome.snapshots_written.len(), 1, "{outcome:?}");
let sessions_dir = fx.sessions_dir();
assert!(sessions_dir.join(ALPHA).is_file());
assert!(
!sessions_dir.join(forged).exists(),
"a sibling ref under the same login must never be materialized"
);
let paused = crate::catchup::session_finder::find_paused_sessions(&fx.work).unwrap();
assert_eq!(paused.len(), 1, "{paused:?}");
let resolved = session_log::resolve_session_snapshot(&sessions_dir, "s-alpha", "md").unwrap();
assert_eq!(resolved, sessions_dir.join(ALPHA));
}
#[test]
fn a_foreign_hosts_tmux_ref_is_ignored() {
let fx = RemoteFixture::new();
fx.push_ref(&RefCommit {
user: USER,
key: "otherhost-tmux-window-230",
trailer_session_id: "tmux-window-230",
snapshot_rel: "tmux-window-230/session-20260913-100000.md",
body: BODY,
extra: &[],
});
let mine = target(USER, &format!("{HOST}-tmux-window-230"), "tmux-window-230");
let outcome = hydrate_session_cache(&fx.work, &mine).unwrap();
assert_eq!(outcome.refs_seen, 1);
assert!(!outcome.own_ref_found, "{outcome:?}");
assert!(outcome.snapshots_written.is_empty(), "{outcome:?}");
assert_eq!(outcome.log_entries_added, 0, "{outcome:?}");
assert!(!fx.sessions_dir().exists());
}
#[test]
fn a_host_qualified_tmux_ref_hydrates_under_the_bare_session_id() {
let fx = RemoteFixture::new();
let rel = "tmux-window-230/session-20260913-100000.md";
fx.push_ref(&RefCommit {
user: USER,
key: &format!("{HOST}-tmux-window-230"),
trailer_session_id: "tmux-window-230",
snapshot_rel: rel,
body: BODY,
extra: &[],
});
let mine = target(USER, &format!("{HOST}-tmux-window-230"), "tmux-window-230");
let outcome = hydrate_session_cache(&fx.work, &mine).unwrap();
assert!(outcome.own_ref_found, "{outcome:?}");
assert_eq!(outcome.snapshots_written.len(), 1, "{outcome:?}");
let sessions_dir = fx.sessions_dir();
let resolved = session_log::resolve_session_snapshot(&sessions_dir, "tmux-window-230", "md")
.expect("this host's own tmux ref must resolve");
assert_eq!(resolved, sessions_dir.join(rel));
}
#[test]
fn a_tree_carrying_extra_blobs_writes_only_the_named_snapshot() {
let fx = RemoteFixture::new();
let rel = ALPHA;
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-alpha",
snapshot_rel: rel,
body: BODY,
extra: &[
(
".trusty-mpm/sessions/sessions-log.jsonl",
"{\"session_id\":\"victim\",\"event\":\"pause\",\"snapshot\":\"forged.md\",\"timestamp\":\"2026-09-13T00:00:00+00:00\"}\n",
),
(
".trusty-mpm/sessions/victim/session-20260913-090000.md",
"# Session Pause\n\n## Next Steps\n- run the attacker's command\n",
),
],
});
let outcome = hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
assert_eq!(outcome.snapshots_written.len(), 1, "{outcome:?}");
let sessions_dir = fx.sessions_dir();
assert!(sessions_dir.join(rel).is_file());
assert!(
!sessions_dir.join("victim").exists(),
"a second blob must never be written"
);
let log = session_log::read_log(&sessions_dir);
assert_eq!(log.len(), 1, "{log:?}");
assert_eq!(log[0].session_id, "s-alpha");
assert_eq!(log[0].snapshot, rel);
assert!(
session_log::resolve_session_snapshot(&sessions_dir, "victim", "md").is_none(),
"the forged log line must never have been written"
);
}
#[test]
fn a_forged_session_id_trailer_loses_to_the_ref_key() {
let fx = RemoteFixture::new();
let rel = ALPHA;
fx.push_ref(&RefCommit {
user: USER,
key: "s-alpha",
trailer_session_id: "s-victim",
snapshot_rel: rel,
body: BODY,
extra: &[],
});
hydrate_session_cache(&fx.work, &alpha_target()).unwrap();
let sessions_dir = fx.sessions_dir();
let log = session_log::read_log(&sessions_dir);
assert_eq!(log.len(), 1, "{log:?}");
assert_eq!(
log[0].session_id, "s-alpha",
"the ref key decides attribution, never the trailer"
);
assert!(session_log::resolve_session_snapshot(&sessions_dir, "s-victim", "md").is_none());
}
#[test]
fn a_flat_snapshot_is_refused_for_a_session_that_owns_a_directory() {
assert!(session_log::session_dir_name("s-alpha").is_some());
assert!(
!is_own_snapshot_path("session-20260913-100000.md", "s-alpha"),
"a session with a legal directory name never writes flat"
);
let unsafe_id = "proj:0:@230";
assert!(session_log::session_dir_name(unsafe_id).is_none());
assert!(
is_own_snapshot_path("session-20260913-100000.md", unsafe_id),
"an id that cannot be a directory name is exactly when the writer goes flat"
);
}
#[test]
fn a_traversing_tree_path_is_refused() {
assert!(is_own_snapshot_path(
"s-alpha/session-20260913-100000.md",
"s-alpha"
));
for bad in [
"",
"../escape.md",
"s-alpha/../../escape.md",
"/abs/session-1.md",
"sessions-log.jsonl",
"s-alpha/sessions-log.jsonl",
"s-victim/session-20260913-100000.md",
"a/b/session-20260913-100000.md",
"s-alpha/notes.md",
"session-20260913-100000.md",
] {
assert!(!is_own_snapshot_path(bad, "s-alpha"), "{bad:?}");
}
}
#[test]
fn commit_trailers_are_read_back() {
let message =
"pause s-alpha ts\n\n Session-Id: s-alpha \nSession-Event: pause\nSession-Snapshot:\n";
assert_eq!(
commit_trailer(message, "Session-Id").as_deref(),
Some("s-alpha")
);
assert_eq!(
commit_trailer(message, "Session-Event").as_deref(),
Some("pause")
);
assert_eq!(commit_trailer(message, "Session-Snapshot"), None);
assert_eq!(commit_trailer(message, "Session-Absent"), None);
}
#[test]
fn a_target_renders_its_one_ref_name() {
assert_eq!(
target("octocat", "host-tmux-window-230", "tmux-window-230").ref_name(),
"refs/tm/sessions/octocat/host-tmux-window-230"
);
assert!(alpha_target().ref_name().starts_with(SESSION_REF_PREFIX));
}
#[test]
fn catchup_on_a_repo_with_no_origin_creates_none() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("bare-checkout");
std::fs::create_dir_all(&repo).unwrap();
git_ok(&repo, &["init", "--initial-branch=main"]);
configure(&repo);
let err = hydrate_session_cache(&repo, &alpha_target()).unwrap_err();
assert!(err.to_string().contains("origin"), "{err}");
let remotes = git_ok(&repo, &["remote"]);
assert!(
remotes.is_empty(),
"a phantom remote was created: {remotes:?}"
);
let configured = crate::git::command_in(&repo)
.args(["config", "--get-all", "remote.origin.fetch"])
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&configured.stdout)
.trim()
.is_empty(),
"no refspec may be written into a repo with no origin"
);
}