use std::path::{Path, PathBuf};
use tempfile::TempDir;
use super::*;
fn git_ok(dir: &Path, args: &[&str]) -> String {
let out = trusty_common::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()
}
const USER: &str = "octocat";
struct Fixture {
_tmp: TempDir,
remote: PathBuf,
repo: PathBuf,
}
impl Fixture {
fn new() -> Self {
let tmp = TempDir::new().unwrap();
let remote = tmp.path().join("remote.git");
let repo = tmp.path().join("work");
std::fs::create_dir_all(&repo).unwrap();
git_ok(
tmp.path(),
&[
"init",
"--bare",
"--initial-branch=main",
remote.to_str().unwrap(),
],
);
git_ok(&repo, &["init", "--initial-branch=main"]);
git_ok(&repo, &["config", "user.email", "refs@example.invalid"]);
git_ok(&repo, &["config", "user.name", "Ref Tester"]);
git_ok(&repo, &["config", "commit.gpgsign", "false"]);
git_ok(
&repo,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
std::fs::write(repo.join("README.md"), b"seed\n").unwrap();
git_ok(&repo, &["add", "README.md"]);
git_ok(&repo, &["commit", "-m", "seed"]);
git_ok(&repo, &["push", "origin", "main"]);
Self {
_tmp: tmp,
remote,
repo,
}
}
fn snapshot(&self, session_id: &str, stamp: &str, body: &str) -> PathBuf {
let path = sessions_store(&self.repo)
.join(session_id)
.join(format!("session-{stamp}.md"));
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, body).unwrap();
path
}
fn request<'a>(&'a self, session_id: &'a str, snapshot: &'a Path) -> SessionRefRequest<'a> {
SessionRefRequest {
repo: &self.repo,
session_id,
snapshot_path: snapshot,
timestamp: chrono::Utc::now(),
}
}
fn ls_remote(&self, pattern: &str) -> Vec<(String, String)> {
git_ok(&self.repo, &["ls-remote", "origin", pattern])
.lines()
.filter_map(|l| l.split_once('\t'))
.map(|(sha, name)| (sha.trim().to_string(), name.trim().to_string()))
.collect()
}
fn reject_pushes(&self) {
let hooks = self.remote.join("hooks");
std::fs::create_dir_all(&hooks).unwrap();
let hook = hooks.join("pre-receive");
std::fs::write(&hook, "#!/bin/sh\necho 'refused by policy' >&2\nexit 1\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
}
const PLAIN: &str = "# Session Pause - 2026-09-13T10:00:00+00:00\n\n## Summary\nParked mid-review.\n\n## Git Context\nBranch: feat/7830-session-history-git-refs\n";
#[test]
fn a_first_pause_creates_an_orphan_commit_and_leaves_main_alone() {
let fx = Fixture::new();
let main_before = fx.ls_remote("refs/heads/main");
assert_eq!(main_before.len(), 1);
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let out = publish_session_ref_as(&fx.request("s-alpha", &snap), Some(USER)).unwrap();
assert_eq!(out.ref_name, "refs/tm/sessions/octocat/s-alpha");
assert_eq!(out.parent, None, "the chain's first commit is an orphan");
let refs = fx.ls_remote("refs/tm/sessions/*");
assert_eq!(refs.len(), 1, "{refs:?}");
assert_eq!(refs[0], (out.commit.clone(), out.ref_name.clone()));
assert_eq!(
fx.ls_remote("refs/heads/main"),
main_before,
"a session ref write must never move a branch"
);
assert_eq!(
git_ok(&fx.repo, &["rev-list", "--count", &out.commit]),
"1",
"the commit shares no history with main"
);
let tree = git_ok(&fx.repo, &["ls-tree", "-r", "--name-only", &out.commit]);
assert_eq!(
tree,
".trusty-mpm/sessions/s-alpha/session-20260913-100000.md"
);
}
#[test]
fn a_second_pause_appends_to_the_same_ref() {
let fx = Fixture::new();
let first_snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let first = publish_session_ref_as(&fx.request("s-alpha", &first_snap), Some(USER)).unwrap();
let second_snap = fx.snapshot("s-alpha", "20260913-110000", PLAIN);
let second = publish_session_ref_as(&fx.request("s-alpha", &second_snap), Some(USER)).unwrap();
assert_eq!(second.ref_name, first.ref_name, "same session, same ref");
assert_eq!(second.parent.as_deref(), Some(first.commit.as_str()));
let refs = fx.ls_remote("refs/tm/sessions/*");
assert_eq!(refs.len(), 1, "one session is still one ref: {refs:?}");
assert_eq!(refs[0].0, second.commit);
assert_eq!(
git_ok(&fx.repo, &["rev-list", "--count", &second.commit]),
"2"
);
}
#[test]
fn a_stale_lease_is_rejected_and_never_forced() {
let fx = Fixture::new();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let landed = publish_session_ref_as(&fx.request("s-alpha", &snap), Some(USER)).unwrap();
let tree = git_ok(
&fx.repo,
&["rev-parse", &format!("{}^{{tree}}", landed.commit)],
);
let racing = git_ok(
&fx.repo,
&["commit-tree", "--no-gpg-sign", &tree, "-m", "racing pause"],
);
git_ok(&fx.repo, &["update-ref", &landed.ref_name, &racing]);
let err = push_session_ref(&fx.repo, &landed.ref_name, None).unwrap_err();
let RefPublishError::StaleLease { ref_name, expected } = &err else {
panic!("expected a stale lease, got {err:?}");
};
assert_eq!(ref_name, &landed.ref_name);
assert_eq!(expected, "<absent>");
let refs = fx.ls_remote("refs/tm/sessions/*");
assert_eq!(
refs[0].0, landed.commit,
"a rejected lease must leave the remote tip exactly where it was"
);
}
#[test]
fn a_rejecting_remote_surfaces_the_push_failure() {
let fx = Fixture::new();
fx.reject_pushes();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let err = publish_session_ref_as(&fx.request("s-alpha", &snap), Some(USER)).unwrap_err();
let message = err.to_string();
assert!(
message.contains("refused by policy"),
"the remote's own reason must reach the caller: {message}"
);
assert!(fx.ls_remote("refs/tm/sessions/*").is_empty());
}
#[test]
fn a_credential_in_the_snapshot_refuses_the_publish() {
let fx = Fixture::new();
let body = format!(
"{PLAIN}\n## Next Steps\n- rotate ghp_{}\n",
"A1b2C3d4E5f6G7h8i9"
);
let snap = fx.snapshot("s-alpha", "20260913-100000", &body);
let err = publish_session_ref_as(&fx.request("s-alpha", &snap), Some(USER)).unwrap_err();
let RefPublishError::CredentialDetected(hit) = &err else {
panic!("expected a credential refusal, got {err:?}");
};
assert_eq!(hit, "ghp_…", "the preview must not echo the secret");
assert!(fx.ls_remote("refs/tm/sessions/*").is_empty());
assert!(snap.is_file(), "the local cache keeps the snapshot");
}
#[test]
fn an_ordinary_snapshot_is_not_flagged() {
assert_eq!(scan_for_credentials(PLAIN), None);
for benign in [
"Last commit: acfd47e369497a4e234d073b17d28817d28efe0d feat(x): y",
"the AKIA prefix is what the scanner keys on",
"see https://github.com/bobmatnyc/trusty-tools/pull/7831",
"sk-ant-",
] {
assert_eq!(scan_for_credentials(benign), None, "{benign:?}");
}
assert!(scan_for_credentials("-----BEGIN RSA PRIVATE KEY-----").is_some());
}
#[test]
fn a_missing_user_id_skips_the_publish() {
let fx = Fixture::new();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let err = publish_session_ref_as(&fx.request("s-alpha", &snap), None).unwrap_err();
assert!(matches!(err, RefPublishError::NoUserId), "{err:?}");
assert!(fx.ls_remote("refs/tm/sessions/*").is_empty());
assert!(snap.is_file());
}
#[test]
fn a_disabled_section_publishes_nothing() {
let fx = Fixture::new();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let fetch_before = git_ok(&fx.repo, &["config", "--get-all", "remote.origin.fetch"]);
let receipt = publish_receipt(&fx.request("s-alpha", &snap), false);
assert_eq!(receipt, SessionRefReceipt::default());
assert!(!receipt.published);
assert_eq!(receipt.ref_name, None);
assert_eq!(receipt.error, None);
assert!(fx.ls_remote("refs/tm/sessions/*").is_empty());
assert_eq!(
trusty_common::catchup::session_refs::list_session_refs(&fx.repo).unwrap(),
vec![]
);
assert_eq!(
git_ok(&fx.repo, &["config", "--get-all", "remote.origin.fetch"]),
fetch_before,
"a disabled section must not register the fetch refspec"
);
}
#[test]
fn an_enabled_receipt_names_the_ref_it_published() {
let fx = Fixture::new();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let expected = session_ref_for(&fx.repo, "s-alpha")
.expect("the fixture pins `git config user.name`, so a user id always resolves");
let receipt = publish_receipt(&fx.request("s-alpha", &snap), true);
assert!(receipt.published, "{receipt:?}");
assert_eq!(receipt.ref_name.as_deref(), Some(expected.as_str()));
assert_eq!(receipt.error, None);
assert_eq!(fx.ls_remote("refs/tm/sessions/*").len(), 1);
}
#[test]
fn ref_components_are_sanitized_and_rejected() {
assert_eq!(
sanitize_ref_component("octocat").as_deref(),
Some("octocat")
);
assert_eq!(
sanitize_ref_component("Robert (Masa) Matsuoka").as_deref(),
Some("RobertMasaMatsuoka"),
);
assert_eq!(
sanitize_ref_component("tmux-window-230").as_deref(),
Some("tmux-window-230")
);
for bad in [
"", " ", ".", "..", "a..b", ".hidden", "-lead", "trail.", "x.lock", "@@@",
] {
assert_eq!(sanitize_ref_component(bad), None, "{bad:?}");
}
}
#[test]
fn a_tmux_session_key_is_hostname_qualified() {
let managed = "7bd5c27a-475b-41df-9e9f-a6f630801717";
assert_eq!(session_key(managed).as_deref(), Some(managed));
let key = session_key("tmux-window-230").expect("a tmux id always keys a ref");
assert!(key.ends_with("tmux-window-230"), "{key}");
if let Some(host) = sysinfo::System::host_name().and_then(|h| sanitize_ref_component(&h)) {
assert_eq!(key, format!("{host}-tmux-window-230"));
assert_ne!(key, "tmux-window-230", "two hosts must not share one ref");
}
}
#[test]
fn a_snapshot_outside_the_store_is_refused() {
let fx = Fixture::new();
let stray = fx.repo.join("session-20260913-100000.md");
std::fs::write(&stray, PLAIN).unwrap();
let err = publish_session_ref_as(&fx.request("s-alpha", &stray), Some(USER)).unwrap_err();
assert!(
matches!(err, RefPublishError::SnapshotOutsideStore(_)),
"{err:?}"
);
}
#[test]
fn the_ref_commit_carries_the_attribution_trailers() {
let fx = Fixture::new();
let snap = fx.snapshot("s-alpha", "20260913-100000", PLAIN);
let out = publish_session_ref_as(&fx.request("s-alpha", &snap), Some(USER)).unwrap();
let message = git_ok(&fx.repo, &["cat-file", "commit", &out.commit]);
assert!(message.contains("Session-Id: s-alpha"), "{message}");
assert!(message.contains("Session-Event: pause"), "{message}");
assert!(
message.contains("Session-Snapshot: s-alpha/session-20260913-100000.md"),
"{message}"
);
assert!(message.contains("Session-Timestamp: "), "{message}");
}