use std::cell::RefCell;
use std::path::{Path, PathBuf};
use chrono::TimeZone as _;
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Call {
program: &'static str,
args: Vec<String>,
index_file: Option<PathBuf>,
}
struct FakeVcs {
script: Vec<(Vec<String>, CmdOut)>,
calls: RefCell<Vec<Call>>,
bodies: RefCell<Vec<(PathBuf, String)>>,
}
impl FakeVcs {
fn new() -> Self {
Self {
script: Vec::new(),
calls: RefCell::new(Vec::new()),
bodies: RefCell::new(Vec::new()),
}
}
fn ok(mut self, prefix: &[&str], stdout: &str) -> Self {
self.script.push((
prefix.iter().map(|s| (*s).to_string()).collect(),
CmdOut {
success: true,
stdout: stdout.to_string(),
stderr: String::new(),
},
));
self
}
fn fail(mut self, prefix: &[&str], stderr: &str) -> Self {
self.script.push((
prefix.iter().map(|s| (*s).to_string()).collect(),
CmdOut {
success: false,
stdout: String::new(),
stderr: stderr.to_string(),
},
));
self
}
fn respond(&self, args: &[String]) -> CmdOut {
let mut best: Option<&(Vec<String>, CmdOut)> = None;
for entry in &self.script {
if args.len() >= entry.0.len()
&& args[..entry.0.len()] == entry.0[..]
&& best.is_none_or(|b| entry.0.len() >= b.0.len())
{
best = Some(entry);
}
}
best.map(|e| e.1.clone()).unwrap_or(CmdOut {
success: true,
stdout: String::new(),
stderr: String::new(),
})
}
fn calls(&self) -> Vec<Call> {
self.calls.borrow().clone()
}
fn git_argv(&self) -> Vec<String> {
self.calls()
.into_iter()
.filter(|c| c.program == "git")
.map(|c| c.args.join(" "))
.collect()
}
fn index_files(&self) -> Vec<PathBuf> {
let mut seen: Vec<PathBuf> = self
.calls()
.into_iter()
.filter_map(|c| c.index_file)
.collect();
seen.sort();
seen.dedup();
seen
}
}
impl PauseVcs for FakeVcs {
fn git(
&self,
_repo: &Path,
args: &[String],
index_file: Option<&Path>,
) -> anyhow::Result<CmdOut> {
self.calls.borrow_mut().push(Call {
program: "git",
args: args.to_vec(),
index_file: index_file.map(Path::to_path_buf),
});
Ok(self.respond(args))
}
fn tm(&self, _repo: &Path, args: &[String]) -> anyhow::Result<CmdOut> {
self.calls.borrow_mut().push(Call {
program: "tm",
args: args.to_vec(),
index_file: None,
});
if let Some(i) = args.iter().position(|a| a == "--body-file") {
let path = PathBuf::from(&args[i + 1]);
let body = std::fs::read_to_string(&path).unwrap_or_default();
self.bodies.borrow_mut().push((path, body));
}
Ok(self.respond(args))
}
}
fn happy() -> FakeVcs {
FakeVcs::new()
.ok(&["rev-parse", "--abbrev-ref", "HEAD"], "main\n")
.ok(
&["rev-parse", "--abbrev-ref", "origin/HEAD"],
"origin/main\n",
)
.fail(&["check-ignore"], "")
.ok(&["rev-parse", "origin/main"], "base000\n")
.ok(&["rev-parse", "base000^{tree}"], "basetree0\n")
.ok(&["hash-object"], "blob111\n")
.ok(&["write-tree"], "newtree9\n")
.ok(&["commit-tree"], "c0ffee1\n")
.ok(
&["pr", "open"],
"opened PR #4242 — https://github.com/bobmatnyc/trusty-tools/pull/4242\n",
)
.ok(&["pr", "merge"], "auto-merge armed on #4242\n")
}
fn ts() -> chrono::DateTime<chrono::Utc> {
chrono::Utc
.with_ymd_and_hms(2026, 9, 9, 18, 30, 15)
.unwrap()
}
fn request<'a>(dir: &'a Path, paths: Vec<String>) -> PublishRequest<'a> {
PublishRequest {
repo: dir,
session_id: "trusty-tools-95",
session_name: None,
timestamp: ts(),
paths,
default_branch: None,
}
}
fn request_on<'a>(dir: &'a Path, paths: Vec<String>, branch: &'a str) -> PublishRequest<'a> {
PublishRequest {
default_branch: Some(branch),
..request(dir, paths)
}
}
fn snapshot_paths() -> Vec<String> {
vec![
".trusty-mpm/sessions/abc/session-20260909-183015.md".to_string(),
".trusty-mpm/sessions/sessions-log.jsonl".to_string(),
]
}
fn pr_open_session(vcs: &FakeVcs) -> String {
let call = vcs
.calls()
.into_iter()
.find(|c| c.program == "tm" && c.args.first().is_some_and(|a| a == "pr"))
.expect("the publish must call `tm pr open`");
let at = call
.args
.iter()
.position(|a| a == "--session")
.expect("`tm pr open` must be given a session");
call.args[at + 1].clone()
}
#[test]
fn the_workstream_label_comes_from_the_session_name() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let uuid = "b175bb88-af7f-5ba5-b75d-85795d60b234";
let req = PublishRequest {
session_id: uuid,
session_name: Some("tm-agents"),
..request(dir.path(), snapshot_paths())
};
publish_pause_snapshot(&vcs, &req)
.unwrap()
.expect("a changed tree publishes");
let session = pr_open_session(&vcs);
assert_eq!(session, "tm-agents", "the NAME, not the managed id");
assert_ne!(session, uuid);
let label = crate::core::policy_labels::workstream_label(&session)
.expect("a non-blank name derives a label");
assert_eq!(label.name, "ws/tm-agents");
}
#[test]
fn an_unnamed_session_falls_back_to_the_session_id() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let req = PublishRequest {
session_name: Some(" "),
..request(dir.path(), snapshot_paths())
};
publish_pause_snapshot(&vcs, &req)
.unwrap()
.expect("a changed tree publishes");
assert_eq!(pr_open_session(&vcs), "trusty-tools-95");
}
#[test]
fn publish_branch_name_carries_session_and_timestamp() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("a changed tree publishes");
assert_eq!(out.branch, "chore/sessions-trusty-tools-95-20260909-183015");
assert_eq!(out.commit, "c0ffee1");
assert_eq!(
out.pr_url,
"https://github.com/bobmatnyc/trusty-tools/pull/4242"
);
assert!(out.auto_merge_armed);
let argv = vcs.git_argv();
assert!(
argv.iter()
.any(|a| a.starts_with("commit-tree newtree9 -p base000")),
"{argv:?}"
);
assert!(
argv.iter()
.any(|a| a
== "update-ref refs/heads/chore/sessions-trusty-tools-95-20260909-183015 c0ffee1"),
"{argv:?}"
);
assert!(
argv.iter()
.any(|a| a.starts_with("push origin refs/heads/")),
"{argv:?}"
);
}
#[test]
fn publish_commits_only_the_allowlisted_paths() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap();
let argv = vcs.git_argv();
assert!(
!argv.iter().any(|a| a.starts_with("add")),
"a pause must never run `git add`: {argv:?}"
);
assert!(
!argv
.iter()
.any(|a| a.contains("stash") || a.contains("checkout")),
"a pause must not stash or check anything out: {argv:?}"
);
let staged: Vec<&String> = argv
.iter()
.filter(|a| a.starts_with("update-index"))
.collect();
assert_eq!(staged.len(), 2, "{argv:?}");
assert_eq!(
staged[0],
"update-index --add --cacheinfo 100644,blob111,.trusty-mpm/sessions/abc/session-20260909-183015.md"
);
assert_eq!(
staged[1],
"update-index --add --cacheinfo 100644,blob111,.trusty-mpm/sessions/sessions-log.jsonl"
);
for call in vcs.calls().iter().filter(|c| c.program == "git") {
let verb = call.args.first().map(String::as_str).unwrap_or("");
if matches!(verb, "read-tree" | "update-index" | "write-tree") {
assert!(
call.index_file.is_some(),
"`{verb}` ran against the shared index"
);
}
}
}
#[test]
fn publish_rejects_a_path_outside_the_sessions_tree() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let paths = vec![
".trusty-mpm/sessions/abc/session-1.md".to_string(),
"crates/trusty-mpm/src/lib.rs".to_string(),
];
let err = publish_pause_snapshot(&vcs, &request(dir.path(), paths)).unwrap_err();
assert!(
matches!(
&err,
PublishError::Step {
step: "allowlist",
..
}
),
"{err:?}"
);
assert!(
err.to_string().contains("crates/trusty-mpm/src/lib.rs"),
"{err}"
);
assert!(
vcs.calls().is_empty(),
"nothing may be spawned once the allowlist rejects a path"
);
}
#[test]
fn a_feature_branch_checkout_still_publishes() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().ok(
&["rev-parse", "--abbrev-ref", "HEAD"],
"fix/7282-something\n",
);
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("a feature-branch checkout publishes");
assert_eq!(out.branch, "chore/sessions-trusty-tools-95-20260909-183015");
assert_eq!(out.commit, "c0ffee1");
let argv = vcs.git_argv();
assert!(
argv.iter()
.any(|a| a.starts_with("commit-tree newtree9 -p base000")),
"{argv:?}"
);
assert!(
!argv.iter().any(|a| a.contains("fix/7282-something")),
"the checkout's branch may not reach any git step: {argv:?}"
);
let tm = tm_call(&vcs, 0);
assert_eq!(
flag_value(&tm, "--head").as_deref(),
Some(out.branch.as_str())
);
assert_eq!(flag_value(&tm, "--base").as_deref(), Some("main"));
}
#[test]
fn publish_uses_the_configured_default_branch() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy()
.ok(&["rev-parse", "--abbrev-ref", "HEAD"], "develop\n")
.ok(&["rev-parse", "origin/develop"], "base000\n");
let out = publish_pause_snapshot(&vcs, &request_on(dir.path(), snapshot_paths(), "develop"))
.unwrap()
.expect("a changed tree publishes");
assert_eq!(out.commit, "c0ffee1");
let argv = vcs.git_argv();
assert!(
argv.contains(&"fetch origin develop".to_string()),
"{argv:?}"
);
assert!(
argv.contains(&"rev-parse origin/develop".to_string()),
"{argv:?}"
);
assert!(
!argv
.iter()
.any(|a| a.contains("origin/HEAD") || a.contains("origin/main")),
"a configured branch is authoritative; nothing else may be consulted: {argv:?}"
);
let tm = vcs
.calls()
.into_iter()
.find(|c| c.program == "tm")
.unwrap()
.args;
assert_eq!(
tm[tm.iter().position(|a| a == "--base").unwrap() + 1],
"develop"
);
}
#[test]
fn publish_falls_back_to_origin_head_for_the_default_branch() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy()
.ok(&["rev-parse", "--abbrev-ref", "HEAD"], "develop\n")
.ok(
&["rev-parse", "--abbrev-ref", "origin/HEAD"],
"origin/develop\n",
)
.ok(&["rev-parse", "origin/develop"], "base000\n");
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("a changed tree publishes");
assert_eq!(out.commit, "c0ffee1");
assert!(
vcs.git_argv().contains(&"fetch origin develop".to_string()),
"{:?}",
vcs.git_argv()
);
}
#[test]
fn publish_falls_back_to_main_when_nothing_names_a_branch() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy()
.ok(&["rev-parse", "--abbrev-ref", "HEAD"], "master\n")
.fail(
&["rev-parse", "--abbrev-ref", "origin/HEAD"],
"fatal: ambiguous argument 'origin/HEAD'",
);
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("a changed tree publishes");
assert_eq!(out.commit, "c0ffee1");
let argv = vcs.git_argv();
assert!(argv.contains(&"fetch origin main".to_string()), "{argv:?}");
assert!(
!argv.iter().any(|a| a.contains("master")),
"the checkout's branch is not a default-branch answer: {argv:?}"
);
assert_eq!(
flag_value(&tm_call(&vcs, 0), "--base").as_deref(),
Some("main")
);
}
#[test]
fn push_failure_leaves_the_commit_and_names_it() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().fail(&["push"], "remote rejected: no write access");
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
let PublishError::Step {
step,
branch,
commit,
..
} = &err
else {
panic!("{err:?}");
};
assert_eq!(*step, "push");
assert_eq!(
branch.as_deref(),
Some("chore/sessions-trusty-tools-95-20260909-183015")
);
assert_eq!(commit.as_deref(), Some("c0ffee1"));
let text = err.to_string();
assert!(text.contains("remote rejected"), "{text}");
assert!(text.contains("push it and open its PR by hand"), "{text}");
assert!(
vcs.git_argv().iter().any(|a| a.starts_with("update-ref ")),
"the branch must still have been created"
);
assert!(
!vcs.calls().iter().any(|c| c.program == "tm"),
"{:?}",
vcs.calls()
);
}
#[test]
fn pr_open_failure_reports_the_pushed_branch() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().fail(&["pr", "open"], "check failed: workstream label");
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
let PublishError::Step { step, commit, .. } = &err else {
panic!("{err:?}");
};
assert_eq!(*step, "pr-open");
assert_eq!(commit.as_deref(), Some("c0ffee1"));
assert!(err.to_string().contains("workstream label"), "{err}");
}
#[test]
fn publish_opens_the_pr_through_tm_pr_open() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap();
let tm: Vec<Vec<String>> = vcs
.calls()
.into_iter()
.filter(|c| c.program == "tm")
.map(|c| c.args)
.collect();
assert_eq!(tm.len(), 2, "{tm:?}");
assert_eq!(tm[0][0..2], ["pr".to_string(), "open".to_string()]);
assert!(tm[0].contains(&"--docs-only".to_string()), "{:?}", tm[0]);
assert!(tm[0].contains(&"--rung".to_string()), "{:?}", tm[0]);
assert_eq!(
tm[0][tm[0].iter().position(|a| a == "--title").unwrap() + 1],
"chore(sessions): pause snapshot for trusty-tools-95 2026-09-09 18:30Z"
);
assert_eq!(
tm[1],
vec![
"pr".to_string(),
"merge".to_string(),
"4242".to_string(),
"--auto".to_string()
]
);
let (body_path, body) = vcs.bodies.borrow()[0].clone();
for heading in [
"## Outcome",
"## Changes",
"## Risk",
"## Tests",
"## Baseline",
"## Gates not run",
"## Partial-red accounting",
"## Docs",
"## Review",
] {
assert!(body.contains(heading), "body is missing {heading}:\n{body}");
}
assert!(body.trim_end().ends_with(ATTRIBUTION_FOOTER), "{body}");
assert!(
!body.to_lowercase().contains("closes #"),
"a session PR closes nothing: {body}"
);
assert!(
!body_path.exists(),
"the scratch body must not outlive the publish: {}",
body_path.display()
);
}
#[test]
fn pr_open_names_the_chore_branch_as_the_head() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("the happy path publishes");
let open = tm_call(&vcs, 0);
assert_eq!(
flag_value(&open, "--head").as_deref(),
Some(out.branch.as_str()),
"{open:?}"
);
assert_eq!(
flag_value(&open, "--base").as_deref(),
Some("main"),
"{open:?}"
);
}
#[test]
fn the_pushed_branch_is_the_head_the_pr_opens_from() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("the happy path publishes");
let branch = &out.branch;
let calls = vcs.calls();
let push_at = calls
.iter()
.position(|c| {
c.program == "git"
&& c.args.first().map(String::as_str) == Some("push")
&& c.args
.contains(&format!("refs/heads/{branch}:refs/heads/{branch}"))
})
.expect("the exact branch must be pushed");
let open_at = calls
.iter()
.position(|c| c.program == "tm" && c.args.first().map(String::as_str) == Some("pr"))
.expect("the PR must be opened");
assert!(
push_at < open_at,
"the push must precede pr-open: {calls:?}"
);
assert_eq!(calls[push_at].args[1], "origin", "{:?}", calls[push_at]);
}
#[test]
fn a_dirty_checkout_is_never_read_staged_or_switched() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().ok(&["rev-parse", "--abbrev-ref", "HEAD"], "fix/some-work\n");
publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap();
for argv in vcs.git_argv() {
let verb = argv.split(' ').next().unwrap_or_default();
assert!(
!matches!(
verb,
"add" | "stash" | "checkout" | "switch" | "status" | "commit"
),
"the publish must not touch the checkout: `git {argv}`"
);
}
}
#[test]
fn a_detached_checkout_is_refused_by_name() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().ok(&["rev-parse", "--abbrev-ref", "HEAD"], "HEAD\n");
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
assert_eq!(
err,
PublishError::NotOnDefaultBranch {
expected: "main".to_string(),
actual: "HEAD".to_string(),
}
);
assert!(err.to_string().contains("detached checkout"), "{err}");
assert!(
err.to_string().contains("The snapshot file was written"),
"{err}"
);
assert!(
!vcs.calls().iter().any(|c| c.program == "tm"),
"no PR may be opened from a detached checkout: {:?}",
vcs.calls()
);
}
fn tm_call(vcs: &FakeVcs, n: usize) -> Vec<String> {
vcs.calls()
.into_iter()
.filter(|c| c.program == "tm")
.map(|c| c.args)
.nth(n)
.unwrap_or_else(|| panic!("no tm call #{n}"))
}
fn flag_value(argv: &[String], flag: &str) -> Option<String> {
let at = argv.iter().position(|a| a == flag)?;
argv.get(at + 1).cloned()
}
#[test]
fn auto_merge_failure_is_reported_without_failing_the_publish() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().fail(
&["pr", "merge"],
"auto-merge is not enabled for bobmatnyc/trusty-tools",
);
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("an unarmed PR is still a published PR");
assert!(!out.auto_merge_armed, "{out:?}");
assert_eq!(
out.auto_merge_error.as_deref(),
Some("auto-merge is not enabled for bobmatnyc/trusty-tools"),
"{out:?}"
);
assert_eq!(out.branch, "chore/sessions-trusty-tools-95-20260909-183015");
assert_eq!(out.commit, "c0ffee1");
assert_eq!(
out.pr_url,
"https://github.com/bobmatnyc/trusty-tools/pull/4242"
);
}
#[test]
fn a_successful_arming_carries_no_auto_merge_error() {
let dir = tempfile::TempDir::new().unwrap();
let out = publish_pause_snapshot(&happy(), &request(dir.path(), snapshot_paths()))
.unwrap()
.expect("the happy path opens a PR");
assert!(out.auto_merge_armed, "{out:?}");
assert_eq!(out.auto_merge_error, None, "{out:?}");
}
#[test]
fn concurrent_publishes_never_share_a_scratch_index() {
let indexes: Vec<PathBuf> = std::thread::scope(|s| {
let handles: Vec<_> = (0..8)
.map(|_| {
s.spawn(|| {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy();
publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap();
let idx = vcs.index_files();
assert_eq!(idx.len(), 1, "one publish uses one index: {idx:?}");
idx.into_iter().next().unwrap()
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
let mut unique = indexes.clone();
unique.sort();
unique.dedup();
assert_eq!(
unique.len(),
indexes.len(),
"concurrent publishes shared a scratch index: {indexes:?}"
);
}
#[test]
fn an_early_failure_leaves_no_scratch_file_behind() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().fail(&["write-tree"], "fatal: unable to write new index file");
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
assert!(
matches!(
err,
PublishError::Step {
step: "write-tree",
..
}
),
"{err:?}"
);
let idx = vcs.index_files();
assert_eq!(idx.len(), 1, "{idx:?}");
assert!(
!idx[0].exists(),
"scratch index survived: {}",
idx[0].display()
);
assert!(
!idx[0].parent().unwrap().exists(),
"scratch directory survived: {}",
idx[0].parent().unwrap().display()
);
}
#[test]
fn publish_rejects_a_snapshot_path_that_is_not_a_regular_file() {
let dir = tempfile::TempDir::new().unwrap();
let sessions = dir.path().join(".trusty-mpm/sessions");
std::fs::create_dir_all(&sessions).unwrap();
std::fs::write(dir.path().join("secret.txt"), b"private").unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(dir.path().join("secret.txt"), sessions.join("linked.md")).unwrap();
#[cfg(not(unix))]
std::fs::create_dir_all(sessions.join("linked.md")).unwrap();
let vcs = happy();
let paths = vec![".trusty-mpm/sessions/linked.md".to_string()];
let err = publish_pause_snapshot(&vcs, &request(dir.path(), paths)).unwrap_err();
assert!(
matches!(
&err,
PublishError::Step {
step: "allowlist",
..
}
),
"{err:?}"
);
assert!(err.to_string().contains("not a regular file"), "{err}");
assert!(
vcs.calls().is_empty(),
"nothing may be spawned once the allowlist rejects a path"
);
}
#[cfg(unix)]
#[test]
fn publish_rejects_a_symlinked_sessions_directory() {
let dir = tempfile::TempDir::new().unwrap();
let outside = tempfile::TempDir::new().unwrap();
std::fs::write(outside.path().join("session-1.md"), b"redirected").unwrap();
std::fs::create_dir_all(dir.path().join(".trusty-mpm")).unwrap();
std::os::unix::fs::symlink(outside.path(), dir.path().join(".trusty-mpm/sessions")).unwrap();
let vcs = happy();
let paths = vec![".trusty-mpm/sessions/session-1.md".to_string()];
let err = publish_pause_snapshot(&vcs, &request(dir.path(), paths)).unwrap_err();
assert!(
matches!(
&err,
PublishError::Step {
step: "allowlist",
..
}
),
"{err:?}"
);
assert!(err.to_string().contains("symlink"), "{err}");
assert!(
vcs.calls().is_empty(),
"nothing may be spawned once the allowlist rejects a redirected path"
);
}
#[test]
fn publish_skips_a_project_that_ignores_its_sessions() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = FakeVcs::new()
.ok(&["rev-parse", "--abbrev-ref", "HEAD"], "main\n")
.ok(&["check-ignore"], "");
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
assert!(matches!(err, PublishError::NotTracked(_)), "{err:?}");
assert!(
!vcs.git_argv().iter().any(|a| a.starts_with("fetch")),
"nothing may be fetched for an untracked sessions tree"
);
}
#[test]
fn publish_is_a_noop_when_the_tree_is_unchanged() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = happy().ok(&["write-tree"], "basetree0\n");
let out = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap();
assert!(out.is_none(), "{out:?}");
assert!(
!vcs.git_argv().iter().any(|a| a.starts_with("commit-tree")),
"no commit may be created when nothing changed"
);
assert!(!vcs.calls().iter().any(|c| c.program == "tm"));
}
#[test]
fn publish_skips_a_directory_that_is_not_a_git_repo() {
let dir = tempfile::TempDir::new().unwrap();
let vcs = FakeVcs::new().fail(
&["rev-parse", "--abbrev-ref", "HEAD"],
"fatal: not a git repository (or any of the parent directories): .git",
);
let err = publish_pause_snapshot(&vcs, &request(dir.path(), snapshot_paths())).unwrap_err();
assert!(matches!(err, PublishError::NotAGitRepo(_)), "{err:?}");
assert_eq!(vcs.calls().len(), 1, "{:?}", vcs.calls());
}
#[test]
fn slug_bounds_and_sanitizes_the_session_id() {
assert_eq!(super::slug("trusty-tools-95"), "trusty-tools-95");
assert_eq!(super::slug("tmux window/1"), "tmux-window-1");
assert_eq!(super::slug("///"), "session");
assert_eq!(super::slug(&"x".repeat(80)).len(), 40);
}