#![expect(clippy::unwrap_used, reason = "test scaffolding")]
use llmenv::sync;
use std::path::Path;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
fn git(dir: &Path, args: &[&str]) {
let status = std::process::Command::new("git")
.args(args)
.current_dir(dir)
.status()
.unwrap();
assert!(status.success(), "git {args:?} failed in {}", dir.display());
}
fn init_repo(dir: &Path) {
git(dir, &["init", "-q"]);
git(dir, &["config", "user.email", "test@example.com"]);
git(dir, &["config", "user.name", "Test"]);
git(dir, &["config", "commit.gpgsign", "false"]);
}
#[test]
fn sync_state_records_and_reads_last_pull() {
let tmp = TempDir::new().unwrap();
let state_dir = tmp.path();
let now = SystemTime::now();
sync::write_state(state_dir, now).unwrap();
let read = sync::read_state(state_dir).unwrap();
assert!(read.is_some());
let read_time = read.unwrap();
let duration = now
.duration_since(read_time)
.unwrap_or_else(|_| read_time.duration_since(now).unwrap());
assert!(duration < Duration::from_secs(1));
}
#[test]
fn sync_state_returns_none_when_not_exists() {
let tmp = TempDir::new().unwrap();
let state_dir = tmp.path();
let read = sync::read_state(state_dir).unwrap();
assert!(read.is_none());
}
#[test]
fn sync_maybe_pull_respects_interval() {
let tmp = TempDir::new().unwrap();
let state_dir = tmp.path();
let repo_dir = tmp.path();
let now = SystemTime::now();
sync::write_state(state_dir, now).unwrap();
let result = sync::maybe_pull(repo_dir, state_dir, Duration::from_secs(3600));
assert!(result.is_ok());
}
#[test]
fn sync_maybe_pull_pulls_when_interval_elapsed() {
let tmp = TempDir::new().unwrap();
let state_dir = tmp.path();
let repo_dir = tmp.path();
std::process::Command::new("git")
.args(["init"])
.current_dir(repo_dir)
.status()
.unwrap();
let old_time = SystemTime::now() - Duration::from_secs(7200);
sync::write_state(state_dir, old_time).unwrap();
let result = sync::maybe_pull(repo_dir, state_dir, Duration::from_secs(3600));
assert!(result.is_ok());
}
#[test]
fn commit_and_push_reports_nothing_when_clean() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
let outcome = sync::commit_and_push(tmp.path(), "Update llmenv config").unwrap();
assert_eq!(outcome, sync::SyncOutcome::NothingToCommit);
}
#[test]
fn commit_and_push_surfaces_push_failure() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
std::fs::write(tmp.path().join("config.yaml"), b"x: 1\n").unwrap();
let err = sync::commit_and_push(tmp.path(), "Update llmenv config").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("push"),
"push failure should be surfaced: {msg}"
);
}
#[test]
fn commit_and_push_pushes_change_to_remote() {
let tmp = TempDir::new().unwrap();
let remote = tmp.path().join("remote.git");
let work = tmp.path().join("work");
std::fs::create_dir_all(&work).unwrap();
git(
tmp.path(),
&["init", "-q", "--bare", remote.to_str().unwrap()],
);
init_repo(&work);
std::fs::write(work.join("config.yaml"), b"x: 1\n").unwrap();
git(&work, &["add", "-A"]);
git(&work, &["commit", "-q", "-m", "initial"]);
git(
&work,
&["remote", "add", "origin", remote.to_str().unwrap()],
);
git(&work, &["push", "-q", "-u", "origin", "HEAD"]);
std::fs::write(work.join("config.yaml"), b"x: 2\n").unwrap();
let outcome = sync::commit_and_push(&work, "Update llmenv config").unwrap();
assert_eq!(outcome, sync::SyncOutcome::Pushed);
let remote_log = std::process::Command::new("git")
.args(["log", "--oneline", "-1"])
.current_dir(&remote)
.output()
.unwrap();
assert!(remote_log.status.success(), "reading remote log failed");
let remote_head = String::from_utf8_lossy(&remote_log.stdout);
assert!(
remote_head.contains("Update llmenv config"),
"remote HEAD should be the pushed commit, got: {}",
remote_head.trim()
);
}