use std::path::Path;
use super::*;
#[test]
fn glob_match_exact() {
assert!(glob_match(".env", ".env"));
assert!(!glob_match(".env", ".env.local"));
}
#[test]
fn glob_match_prefix() {
assert!(glob_match(".env.*", ".env.local"));
assert!(glob_match(".env.*", ".env.production"));
assert!(!glob_match(".env.*", ".envrc"));
assert!(
!glob_match(".env.*", ".env"),
"trailing '*' requires >=0 more chars after the dot"
);
}
#[test]
fn glob_match_suffix() {
assert!(glob_match("*.env", "prod.env"));
assert!(glob_match("*.env", ".env"));
assert!(!glob_match("*.env", "prod.env.bak"));
}
#[test]
fn glob_match_no_match() {
assert!(!glob_match(".env", "not-env"));
assert!(!glob_match(".env.*", "other-file.txt"));
}
fn make_source_fixture() -> tempfile::TempDir {
let src = tempfile::TempDir::new().expect("src tmp dir");
std::fs::write(src.path().join(".env"), b"SECRET=1").expect("write .env");
std::fs::write(src.path().join(".env.local"), b"SECRET=2").expect("write .env.local");
std::fs::write(src.path().join("random.txt"), b"not a secret").expect("write random.txt");
std::fs::create_dir_all(src.path().join("node_modules/some-pkg")).expect("mkdir node_modules");
std::fs::write(
src.path().join("node_modules/some-pkg/.env"),
b"nested, must not be touched by a top-level pattern",
)
.expect("write nested .env");
let oversized = vec![0u8; (MAX_SYNC_FILE_BYTES + 1) as usize];
std::fs::write(src.path().join(".env.oversized"), &oversized).expect("write oversized file");
src
}
fn default_patterns() -> Vec<String> {
vec![".env".into(), ".env.local".into(), ".env.*".into()]
}
#[test]
fn syncs_matching_files_only() {
let src = make_source_fixture();
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &default_patterns());
assert!(dest.path().join(".env").is_file(), ".env must be copied");
assert!(
dest.path().join(".env.local").is_file(),
".env.local must be copied"
);
assert!(
!dest.path().join("random.txt").exists(),
"non-matching file must NOT be copied"
);
assert!(
!dest.path().join("node_modules").exists(),
"node_modules/ must NOT be copied (directories are never matched)"
);
assert!(
!dest.path().join(".env.oversized").exists(),
"oversized file must be skipped, not copied"
);
assert_eq!(
std::fs::read(dest.path().join(".env")).expect("read copied .env"),
b"SECRET=1"
);
}
#[test]
fn oversized_file_is_skipped() {
let src = tempfile::TempDir::new().expect("src tmp dir");
let oversized = vec![b'x'; (MAX_SYNC_FILE_BYTES + 1) as usize];
std::fs::write(src.path().join(".env"), &oversized).expect("write oversized .env");
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &[".env".to_string()]);
assert!(
!dest.path().join(".env").exists(),
"a file over MAX_SYNC_FILE_BYTES must never be copied"
);
}
#[test]
fn empty_patterns_copies_nothing() {
let src = make_source_fixture();
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &[]);
assert!(
!dest.path().join(".env").exists(),
"an empty allowlist must copy nothing (mirrors the caller's enabled=false gate)"
);
}
#[test]
fn missing_source_root_is_non_fatal() {
let dest = tempfile::TempDir::new().expect("dest tmp dir");
let missing_source = Path::new("/this/path/does/not/exist/anywhere/2196");
sync_untracked_files(missing_source, dest.path(), &default_patterns());
assert!(
std::fs::read_dir(dest.path())
.expect("dest still readable")
.next()
.is_none(),
"dest worktree must stay empty when the source root is missing"
);
}
#[test]
fn missing_source_file_is_non_fatal() {
let src = tempfile::TempDir::new().expect("src tmp dir");
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &[".env.nonexistent".to_string()]);
assert!(
std::fs::read_dir(dest.path())
.expect("dest still readable")
.next()
.is_none(),
"no candidate files exist; sync must be a silent no-op"
);
}
#[test]
fn path_escape_pattern_matches_nothing() {
let src = tempfile::TempDir::new().expect("src tmp dir");
let outside = tempfile::TempDir::new().expect("outside tmp dir");
std::fs::write(outside.path().join(".env"), b"must never be reachable")
.expect("write outside .env");
let dest = tempfile::TempDir::new().expect("dest tmp dir");
let escape_pattern = format!(
"../{}/.env",
outside
.path()
.file_name()
.expect("outside dir has a name")
.to_string_lossy()
);
sync_untracked_files(src.path(), dest.path(), &[escape_pattern]);
assert!(
std::fs::read_dir(dest.path())
.expect("dest still readable")
.next()
.is_none(),
"a pattern with a `..`-escaping directory component must copy nothing"
);
}
#[test]
fn absolute_pattern_matches_nothing() {
let src = tempfile::TempDir::new().expect("src tmp dir");
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &["/etc/.env".to_string()]);
assert!(
std::fs::read_dir(dest.path())
.expect("dest still readable")
.next()
.is_none(),
"an absolute pattern must copy nothing"
);
}
#[test]
fn subdirectory_pattern_matches_one_level_non_recursively() {
let src = tempfile::TempDir::new().expect("src tmp dir");
std::fs::create_dir_all(src.path().join("config")).expect("mkdir config");
std::fs::write(src.path().join("config/.env"), b"nested secret").expect("write config/.env");
std::fs::create_dir_all(src.path().join("config/nested")).expect("mkdir config/nested");
std::fs::write(src.path().join("config/nested/.env"), b"too deep")
.expect("write config/nested/.env");
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &["config/.env".to_string()]);
assert!(
dest.path().join("config/.env").is_file(),
"an explicit subdirectory pattern must copy that one file"
);
assert!(
!dest.path().join("config/nested").exists(),
"a subdirectory pattern must not recurse further than the named directory"
);
}
fn init_base_repo(path: &Path) {
let init = std::process::Command::new("git")
.args(["init", path.to_str().expect("utf8")])
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let ok = std::process::Command::new("git")
.args(["-C", path.to_str().expect("utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(path.join("README"), b"init").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", path.to_str().expect("utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args(["-C", path.to_str().expect("utf8"), "commit", "-m", "init"])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
}
#[test]
fn copied_files_are_added_to_shared_worktree_exclude() {
let base_tmp = tempfile::TempDir::new().expect("base tmp dir");
let base = base_tmp.path();
init_base_repo(base);
let worktree = super::super::create_session_worktree_unchecked(
base,
"untracked-sync-exclude-test",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed against a real, committed base repo");
assert!(
worktree.join(".git").is_file(),
"sanity: a linked worktree's .git must be a FILE (gitlink), not a directory"
);
let src = make_source_fixture();
sync_untracked_files(src.path(), &worktree, &default_patterns());
assert!(worktree.join(".env").is_file(), ".env must be copied");
assert!(worktree.join(".env.local").is_file());
let exclude_out = std::process::Command::new("git")
.arg("-C")
.arg(&worktree)
.args(["rev-parse", "--git-path", "info/exclude"])
.output()
.expect("git rev-parse");
assert!(exclude_out.status.success());
let raw = String::from_utf8_lossy(&exclude_out.stdout)
.trim()
.to_string();
let exclude_path = if Path::new(&raw).is_absolute() {
std::path::PathBuf::from(raw)
} else {
worktree.join(raw)
};
let content = std::fs::read_to_string(&exclude_path).expect("exclude file readable");
assert!(
content.lines().any(|l| l.trim() == ".env"),
"exclude file must list .env: {content}"
);
assert!(
content.lines().any(|l| l.trim() == ".env.local"),
"exclude file must list .env.local: {content}"
);
}
#[test]
fn append_to_git_exclude_is_idempotent() {
let base_tmp = tempfile::TempDir::new().expect("base tmp dir");
let base = base_tmp.path();
init_base_repo(base);
let worktree = super::super::create_session_worktree_unchecked(
base,
"untracked-sync-idempotent-test",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed");
let exclude = match resolve_git_exclude(&worktree) {
ExcludeTarget::Registered(p) => p,
_ => panic!("a real linked worktree must resolve its shared info/exclude path"),
};
append_paths_to_exclude(&exclude, &[".env".to_string()]).expect("first append");
append_paths_to_exclude(&exclude, &[".env".to_string()]).expect("second append");
let exclude_out = std::process::Command::new("git")
.arg("-C")
.arg(&worktree)
.args(["rev-parse", "--git-path", "info/exclude"])
.output()
.expect("git rev-parse");
let raw = String::from_utf8_lossy(&exclude_out.stdout)
.trim()
.to_string();
let exclude_path = if Path::new(&raw).is_absolute() {
std::path::PathBuf::from(raw)
} else {
worktree.join(raw)
};
let content = std::fs::read_to_string(&exclude_path).expect("exclude readable");
let count = content.lines().filter(|l| l.trim() == ".env").count();
assert_eq!(
count, 1,
"repeated appends must not duplicate the entry: {content}"
);
}
#[test]
fn broken_repo_copies_nothing() {
let src = make_source_fixture();
let dest = tempfile::TempDir::new().expect("dest tmp dir");
std::fs::write(dest.path().join(".git"), "gitdir: /nonexistent/xyz-4733\n")
.expect("write stale gitlink");
sync_untracked_files(src.path(), dest.path(), &default_patterns());
assert!(
!dest.path().join(".env").exists(),
"a secret must never be written into a repository git could not confirm \
would ignore it (#4733)"
);
assert!(
!dest.path().join(".env.local").exists(),
".env.local must be refused for the same reason (#4733)"
);
}
#[test]
fn plain_directory_destination_still_copies() {
let src = make_source_fixture();
let dest = tempfile::TempDir::new().expect("dest tmp dir");
sync_untracked_files(src.path(), dest.path(), &default_patterns());
assert!(
dest.path().join(".env").is_file(),
"a non-repository destination has no history to leak into — copying stays allowed"
);
}
#[test]
fn tracked_secret_is_not_overwritten_in_worktree() {
let base_tmp = tempfile::TempDir::new().expect("base tmp dir");
let base = base_tmp.path();
init_base_repo(base);
std::fs::write(base.join(".env"), b"COMMITTED=placeholder").expect("write tracked .env");
for args in [vec!["add", "-A"], vec!["commit", "-m", "track env"]] {
let ok = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8")])
.args(&args)
.status()
.expect("git");
assert!(ok.success(), "git {args:?} failed");
}
let worktree = super::super::create_session_worktree_unchecked(
base,
"untracked-sync-tracked-test",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed");
let src = make_source_fixture();
sync_untracked_files(src.path(), &worktree, &default_patterns());
assert_eq!(
std::fs::read(worktree.join(".env")).expect("tracked .env present in worktree"),
b"COMMITTED=placeholder",
"a TRACKED path is never ignored no matter what info/exclude says — \
overwriting it would stage the operator's real secret (#4733)"
);
assert!(
worktree.join(".env.local").is_file(),
"the untracked sibling is still delivered — the refusal is per-path"
);
}
const PARENT_DIRS_STDERR: &str =
"fatal: not a git repository (or any of the parent directories): .git";
#[test]
fn classify_rev_parse_failure_corroborates_the_no_repo_message() {
let tmp = tempfile::TempDir::new().expect("tmp dir");
let msg = PARENT_DIRS_STDERR;
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), msg),
ExcludeTarget::NoRepo
),
"no ancestor .git witness → the message is believed"
);
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), "fatal: not a git repository: (null)"),
ExcludeTarget::Unknown(_)
),
"the stale-worktree near-miss contains the short phrase but means the opposite"
);
std::fs::write(tmp.path().join(".git"), "gitdir: /somewhere\n").expect("gitlink");
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), msg),
ExcludeTarget::Unknown(_)
),
"a .git witness contradicts the message — a disagreement is 'cannot be asked'"
);
}
#[test]
fn broken_exclude_file_still_refuses_unignored_paths() {
let base_tmp = tempfile::TempDir::new().expect("base tmp dir");
let base = base_tmp.path();
init_base_repo(base);
let worktree = super::super::create_session_worktree_unchecked(
base,
"untracked-sync-broken-exclude-test",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed");
let exclude = match resolve_git_exclude(&worktree) {
ExcludeTarget::Registered(p) => p,
_ => panic!("a real linked worktree must resolve its shared info/exclude path"),
};
let _ = std::fs::remove_file(&exclude);
std::fs::create_dir_all(&exclude).expect("replace info/exclude with a directory");
let src = make_source_fixture();
sync_untracked_files(src.path(), &worktree, &default_patterns());
assert!(
!worktree.join(".env").exists(),
"with registration broken and no .gitignore covering it, `git check-ignore` \
is the only remaining guarantee and it must refuse (#4733)"
);
assert!(
!worktree.join(".env.local").exists(),
".env.local must be refused for the same reason (#4733)"
);
}
#[test]
fn missing_git_binary_copies_nothing() {
let base_tmp = tempfile::TempDir::new().expect("base tmp dir");
let base = base_tmp.path();
init_base_repo(base);
let worktree = super::super::create_session_worktree_unchecked(
base,
"untracked-sync-missing-git-test",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed");
assert!(
matches!(resolve_git_exclude(&worktree), ExcludeTarget::Registered(_)),
"sanity: a real linked worktree resolves with a working git"
);
assert!(
matches!(
resolve_git_exclude_with(&worktree, "trusty-no-such-git-binary-4733"),
ExcludeTarget::Unknown(_)
),
"an unspawnable git answers nothing — it must never be read as 'no repository', \
which would let a secret be copied into a live worktree (#4733)"
);
}
#[test]
fn classify_rev_parse_failure_canonicalises_before_walking_ancestors() {
let tmp = tempfile::TempDir::new().expect("tmp dir");
let repo = tmp.path().join("repo");
std::fs::create_dir_all(repo.join("sub")).expect("mkdir repo/sub");
std::fs::write(repo.join(".git"), "gitdir: /somewhere\n").expect("gitlink");
let link = tmp.path().join("link");
std::os::unix::fs::symlink(repo.join("sub"), &link).expect("symlink");
assert!(
matches!(
classify_rev_parse_failure(&link, PARENT_DIRS_STDERR),
ExcludeTarget::Unknown(_)
),
"the real parent carries a .git — only a canonicalised ancestor walk sees it"
);
}
const MOUNT_BOUNDARY_STDERR: &str = "fatal: not a git repository (or any parent up to mount point /)\n\
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\n";
#[test]
fn classify_rev_parse_failure_accepts_the_mount_boundary_wording() {
let tmp = tempfile::TempDir::new().expect("tmp dir");
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), MOUNT_BOUNDARY_STDERR),
ExcludeTarget::NoRepo
),
"the mount-boundary wording with no .git witness is a genuine absence"
);
for stderr in [
"fatal: not a git repository: (null)",
"fatal: not a git repository: '/nonexistent/x'",
"fatal: detected dubious ownership in repository at '/x'",
"fatal: cannot change to '/x': Permission denied",
] {
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), stderr),
ExcludeTarget::Unknown(_)
),
"unrecognised failure must refuse, not read as no repository: {stderr:?}"
);
}
}
#[test]
fn classify_rev_parse_failure_refuses_the_mount_boundary_wording_under_a_real_repo() {
let tmp = tempfile::TempDir::new().expect("tmp dir");
std::fs::write(tmp.path().join(".git"), "gitdir: /somewhere\n").expect("gitlink");
assert!(
matches!(
classify_rev_parse_failure(tmp.path(), MOUNT_BOUNDARY_STDERR),
ExcludeTarget::Unknown(_)
),
"an ancestor .git contradicts the boundary wording — never copy into a real repo"
);
}